commit all
build-and-notify / Build Backend To GHCR (push) Failing after 1s
build-and-notify / Build Admin UI To GHCR (push) Failing after 0s
build-and-notify / Build Screen To GHCR (push) Failing after 0s
build-and-notify / Notify Sync Service (push) Has been skipped
build-and-notify / Publish Release Metadata (push) Has been skipped
CI / Lint (push) Failing after 0s
CI / Backend (push) Has been skipped
CI / Frontend (push) Has been skipped
Coverage / Backend JaCoCo (push) Failing after 4s
Gate Check / gate (push) Failing after 4s
Post Merge E2E / post_merge_e2e (push) Failing after 4s
Secret Scan / Gitleaks (push) Failing after 0s
Spec Quality Gate / spec-quality-check (push) Failing after 0s
SBOM / generate (push) Failing after 0s
Governance Drift / governance-drift (push) Failing after 1s
Nightly Regression / nightly_regression (push) Failing after 30s
CodeQL / CodeQL Capability Check (push) Has been cancelled
CodeQL / Analyze (java) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / CodeQL Skipped (push) Has been cancelled
build-and-notify / Build Backend To GHCR (push) Failing after 1s
build-and-notify / Build Admin UI To GHCR (push) Failing after 0s
build-and-notify / Build Screen To GHCR (push) Failing after 0s
build-and-notify / Notify Sync Service (push) Has been skipped
build-and-notify / Publish Release Metadata (push) Has been skipped
CI / Lint (push) Failing after 0s
CI / Backend (push) Has been skipped
CI / Frontend (push) Has been skipped
Coverage / Backend JaCoCo (push) Failing after 4s
Gate Check / gate (push) Failing after 4s
Post Merge E2E / post_merge_e2e (push) Failing after 4s
Secret Scan / Gitleaks (push) Failing after 0s
Spec Quality Gate / spec-quality-check (push) Failing after 0s
SBOM / generate (push) Failing after 0s
Governance Drift / governance-drift (push) Failing after 1s
Nightly Regression / nightly_regression (push) Failing after 30s
CodeQL / CodeQL Capability Check (push) Has been cancelled
CodeQL / Analyze (java) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / CodeQL Skipped (push) Has been cancelled
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,26 +11,26 @@ import type {
|
||||
* 获取配置列表(分页)
|
||||
*/
|
||||
export function getConfigList(params?: ConfigQueryDTO): Promise<PageResult<SysConfigVO>> {
|
||||
return request.get<PageResult<SysConfigVO>>('/api/configs', { params })
|
||||
return request.get<PageResult<SysConfigVO>>('/api/v1/configs', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配置
|
||||
*/
|
||||
export function createConfig(data: ConfigCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/configs', data)
|
||||
return request.post<number>('/api/v1/configs', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
export function updateConfig(id: number, data: ConfigUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/configs/${id}`, data)
|
||||
return request.put<void>(`/api/v1/configs/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 key 查询配置
|
||||
*/
|
||||
export function getConfigByKey(key: string): Promise<SysConfigVO> {
|
||||
return request.get<SysConfigVO>(`/api/configs/key/${key}`)
|
||||
return request.get<SysConfigVO>(`/api/v1/configs/key/${key}`)
|
||||
}
|
||||
|
||||
@@ -12,26 +12,26 @@ import type {
|
||||
* 获取字典类型列表(分页)
|
||||
*/
|
||||
export function getDictList(params?: DictTypeQueryDTO): Promise<PageResult<SysDictTypeVO>> {
|
||||
return request.get<PageResult<SysDictTypeVO>>('/api/dicts', { params })
|
||||
return request.get<PageResult<SysDictTypeVO>>('/api/v1/dicts', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建字典类型
|
||||
*/
|
||||
export function createDict(data: DictTypeCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/dicts', data)
|
||||
return request.post<number>('/api/v1/dicts', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典类型
|
||||
*/
|
||||
export function updateDict(id: number, data: DictTypeUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/dicts/${id}`, data)
|
||||
return request.put<void>(`/api/v1/dicts/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型查询字典数据列表
|
||||
*/
|
||||
export function getDictDataByType(type: string): Promise<SysDictDataVO[]> {
|
||||
return request.get<SysDictDataVO[]>(`/api/dicts/type/${type}`)
|
||||
return request.get<SysDictDataVO[]>(`/api/v1/dicts/type/${type}`)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import type { PageResult, SysFileVO, FileQueryDTO } from '@ljwx/shared'
|
||||
* 获取文件列表(分页)
|
||||
*/
|
||||
export function getFileList(params?: FileQueryDTO): Promise<PageResult<SysFileVO>> {
|
||||
return request.get<PageResult<SysFileVO>>('/api/files', { params })
|
||||
return request.get<PageResult<SysFileVO>>('/api/v1/files', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export function uploadFile(formData: FormData): Promise<SysFileVO> {
|
||||
return request.post<SysFileVO>('/api/files/upload', formData, {
|
||||
return request.post<SysFileVO>('/api/v1/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function uploadFile(formData: FormData): Promise<SysFileVO> {
|
||||
* 删除文件
|
||||
*/
|
||||
export function deleteFile(id: number): Promise<void> {
|
||||
return request.delete<void>(`/api/files/${id}`)
|
||||
return request.delete<void>(`/api/v1/files/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,40 +11,40 @@ import type {
|
||||
* 获取定时任务列表(分页)
|
||||
*/
|
||||
export function getJobList(params?: JobQueryDTO): Promise<PageResult<SysJobVO>> {
|
||||
return request.get<PageResult<SysJobVO>>('/api/jobs', { params })
|
||||
return request.get<PageResult<SysJobVO>>('/api/v1/jobs', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建定时任务
|
||||
*/
|
||||
export function createJob(data: JobCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/jobs', data)
|
||||
return request.post<number>('/api/v1/jobs', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务
|
||||
*/
|
||||
export function updateJob(id: number, data: JobUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/jobs/${id}`, data)
|
||||
return request.put<void>(`/api/v1/jobs/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即执行任务
|
||||
*/
|
||||
export function executeJob(id: number): Promise<void> {
|
||||
return request.post<void>(`/api/jobs/${id}/execute`)
|
||||
return request.post<void>(`/api/v1/jobs/${id}/run`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停任务
|
||||
*/
|
||||
export function pauseJob(id: number): Promise<void> {
|
||||
return request.post<void>(`/api/jobs/${id}/pause`)
|
||||
return request.post<void>(`/api/v1/jobs/${id}/pause`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复任务
|
||||
*/
|
||||
export function resumeJob(id: number): Promise<void> {
|
||||
return request.post<void>(`/api/jobs/${id}/resume`)
|
||||
return request.post<void>(`/api/v1/jobs/${id}/resume`)
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ import type {
|
||||
export function getOperationLogList(
|
||||
params?: OperationLogQueryDTO,
|
||||
): Promise<PageResult<OperationLogVO>> {
|
||||
return request.get<PageResult<OperationLogVO>>('/api/logs/operation', { params })
|
||||
return request.get<PageResult<OperationLogVO>>('/api/v1/logs/operation', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录日志列表(分页)
|
||||
*/
|
||||
export function getLoginLogList(params?: LoginLogQueryDTO): Promise<PageResult<LoginLogVO>> {
|
||||
return request.get<PageResult<LoginLogVO>>('/api/logs/login', { params })
|
||||
return request.get<PageResult<LoginLogVO>>('/api/v1/logs/login', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出日志
|
||||
*/
|
||||
export function exportLogs(): Promise<Blob> {
|
||||
return request.post<Blob>('/api/logs/export', undefined, { responseType: 'blob' })
|
||||
return request.post<Blob>('/api/v1/logs/export', undefined, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -11,19 +11,19 @@ import type {
|
||||
* 获取通知列表(分页)
|
||||
*/
|
||||
export function getNoticeList(params?: NoticeQueryDTO): Promise<PageResult<SysNoticeVO>> {
|
||||
return request.get<PageResult<SysNoticeVO>>('/api/notices', { params })
|
||||
return request.get<PageResult<SysNoticeVO>>('/api/v1/notices', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知
|
||||
*/
|
||||
export function createNotice(data: NoticeCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/notices', data)
|
||||
return request.post<number>('/api/v1/notices', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知
|
||||
*/
|
||||
export function updateNotice(id: number, data: NoticeUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/notices/${id}`, data)
|
||||
return request.put<void>(`/api/v1/notices/${id}`, data)
|
||||
}
|
||||
|
||||
@@ -14,35 +14,35 @@ import type {
|
||||
* 获取角色列表(分页)
|
||||
*/
|
||||
export function getRoleList(params?: RoleQueryDTO): Promise<PageResult<RoleVO>> {
|
||||
return request.get<PageResult<RoleVO>>('/api/roles', { params })
|
||||
return request.get<PageResult<RoleVO>>('/api/v1/roles', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
export function createRole(data: RoleCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/roles', data)
|
||||
return request.post<number>('/api/v1/roles', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
export function updateRole(id: number, data: RoleUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/roles/${id}`, data)
|
||||
return request.put<void>(`/api/v1/roles/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
export function deleteRole(id: number): Promise<void> {
|
||||
return request.delete<void>(`/api/roles/${id}`)
|
||||
return request.delete<void>(`/api/v1/roles/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限列表
|
||||
*/
|
||||
export function getPermissionList(): Promise<PermissionVO[]> {
|
||||
return request.get<PermissionVO[]>('/api/permissions')
|
||||
return request.get<PermissionVO[]>('/api/v1/permissions')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,21 +13,21 @@ import type {
|
||||
* 获取租户列表(分页)
|
||||
*/
|
||||
export function getTenantList(params?: TenantQueryDTO): Promise<PageResult<TenantVO>> {
|
||||
return request.get<PageResult<TenantVO>>('/api/tenants', { params })
|
||||
return request.get<PageResult<TenantVO>>('/api/v1/tenants', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户
|
||||
*/
|
||||
export function createTenant(data: TenantCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/tenants', data)
|
||||
return request.post<number>('/api/v1/tenants', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户
|
||||
*/
|
||||
export function updateTenant(id: number, data: TenantUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/tenants/${id}`, data)
|
||||
return request.put<void>(`/api/v1/tenants/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,4 +57,3 @@ export function cancelTenant(id: number, data: TenantCancelDTO): Promise<void> {
|
||||
export function initializeTenant(id: number): Promise<void> {
|
||||
return request.post<void>(`/api/v1/tenants/${id}/initialize`)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,26 +11,26 @@ import type {
|
||||
* 获取用户列表(分页)
|
||||
*/
|
||||
export function getUserList(params?: UserQueryDTO): Promise<PageResult<UserVO>> {
|
||||
return request.get<PageResult<UserVO>>('/api/users', { params })
|
||||
return request.get<PageResult<UserVO>>('/api/v1/users', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
export function createUser(data: UserCreateDTO): Promise<number> {
|
||||
return request.post<number>('/api/users', data)
|
||||
return request.post<number>('/api/v1/users', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
export function updateUser(id: number, data: UserUpdateDTO): Promise<void> {
|
||||
return request.put<void>(`/api/users/${id}`, data)
|
||||
return request.put<void>(`/api/v1/users/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
export function deleteUser(id: number): Promise<void> {
|
||||
return request.delete<void>(`/api/users/${id}`)
|
||||
return request.delete<void>(`/api/v1/users/${id}`)
|
||||
}
|
||||
|
||||
+82
-36
@@ -1,44 +1,90 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const defineStore: typeof import('pinia').defineStore
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getActivePinia: typeof import('pinia').getActivePinia
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
const mapState: typeof import('pinia').mapState
|
||||
const mapStores: typeof import('pinia').mapStores
|
||||
const mapWritableState: typeof import('pinia').mapWritableState
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const storeToRefs: typeof import('pinia').storeToRefs
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useLink: typeof import('vue-router').useLink
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
+85
-4
@@ -1,29 +1,36 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import { GlobalComponents } from 'vue'
|
||||
|
||||
export {}
|
||||
|
||||
/* Prettier disabled */
|
||||
/* TypeScript disabled */
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
@@ -42,19 +49,93 @@ declare module 'vue' {
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
HelpButton: typeof import('./components/HelpButton.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
const ElAside: typeof import('element-plus/es')['ElAside']
|
||||
const ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
const ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
const ElCol: typeof import('element-plus/es')['ElCol']
|
||||
const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
const ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
const ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
const ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
const ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
const ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
const ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
const ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
const ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
const ElImage: typeof import('element-plus/es')['ElImage']
|
||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||
const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
const ElLink: typeof import('element-plus/es')['ElLink']
|
||||
const ElMain: typeof import('element-plus/es')['ElMain']
|
||||
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||
const ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
const ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
const ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
const ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
const ElRow: typeof import('element-plus/es')['ElRow']
|
||||
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
const ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
const ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
const ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
const ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||
const ElText: typeof import('element-plus/es')['ElText']
|
||||
const ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
const ElTree: typeof import('element-plus/es')['ElTree']
|
||||
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
const ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
const HelpButton: typeof import('./components/HelpButton.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
@@ -2,31 +2,136 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { Grid, Odometer } from '@element-plus/icons-vue'
|
||||
import type { RouteRecordNormalized } from 'vue-router'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { Grid, Menu as MenuIcon } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isCollapsed = computed(() => appStore.sidebarCollapsed)
|
||||
|
||||
interface MenuItem {
|
||||
name: string
|
||||
path: string
|
||||
icon: Component
|
||||
interface SidebarItem {
|
||||
index: string
|
||||
path?: string
|
||||
title: string
|
||||
icon: Component
|
||||
children?: SidebarItem[]
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ name: 'Dashboard', path: '/dashboard', icon: Odometer, title: '仪表盘' },
|
||||
]
|
||||
const groupMeta: Record<string, { title: string; icon: Component }> = {
|
||||
system: { title: '系统管理', icon: ElementPlusIconsVue.Setting },
|
||||
monitor: { title: '系统监控', icon: ElementPlusIconsVue.Monitor },
|
||||
ops: { title: '运营中心', icon: ElementPlusIconsVue.DataAnalysis },
|
||||
report: { title: '报表中心', icon: ElementPlusIconsVue.PieChart },
|
||||
workflow: { title: '工作流', icon: ElementPlusIconsVue.Connection },
|
||||
form: { title: '表单中心', icon: ElementPlusIconsVue.EditPen },
|
||||
message: { title: '消息中心', icon: ElementPlusIconsVue.ChatDotRound },
|
||||
billing: { title: '计费中心', icon: ElementPlusIconsVue.Coin },
|
||||
tenant: { title: '租户中心', icon: ElementPlusIconsVue.OfficeBuilding },
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
if (!path) {
|
||||
return '/'
|
||||
}
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
function getRouteTitle(routeRecord: RouteRecordNormalized): string {
|
||||
return String(routeRecord.meta.title || routeRecord.name || routeRecord.path)
|
||||
}
|
||||
|
||||
function resolveIcon(iconName: unknown, fallback: Component = MenuIcon): Component {
|
||||
if (typeof iconName === 'string' && iconName in ElementPlusIconsVue) {
|
||||
return ElementPlusIconsVue[iconName as keyof typeof ElementPlusIconsVue]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getCandidateRoutes(): RouteRecordNormalized[] {
|
||||
if (permissionStore.accessibleRoutes.length > 0) {
|
||||
return permissionStore.accessibleRoutes as RouteRecordNormalized[]
|
||||
}
|
||||
|
||||
return router.getRoutes().filter((routeRecord) => (
|
||||
routeRecord.meta.requiresAuth === true &&
|
||||
routeRecord.path !== '/' &&
|
||||
!routeRecord.redirect
|
||||
))
|
||||
}
|
||||
|
||||
const menuItems = computed<SidebarItem[]>(() => {
|
||||
const grouped = new Map<string, SidebarItem>()
|
||||
const topLevelItems: SidebarItem[] = []
|
||||
|
||||
for (const routeRecord of getCandidateRoutes()) {
|
||||
if (routeRecord.meta.hidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
const absolutePath = normalizePath(routeRecord.path)
|
||||
const segments = absolutePath.replace(/^\/+/, '').split('/').filter(Boolean)
|
||||
|
||||
if (segments.length <= 1) {
|
||||
topLevelItems.push({
|
||||
index: absolutePath,
|
||||
path: absolutePath,
|
||||
title: getRouteTitle(routeRecord),
|
||||
icon: resolveIcon(routeRecord.meta.icon, MenuIcon),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const groupKey = segments[0]
|
||||
const meta = groupMeta[groupKey] ?? {
|
||||
title: groupKey,
|
||||
icon: resolveIcon(routeRecord.meta.icon, MenuIcon),
|
||||
}
|
||||
|
||||
const groupIndex = `group:${groupKey}`
|
||||
if (!grouped.has(groupIndex)) {
|
||||
grouped.set(groupIndex, {
|
||||
index: groupIndex,
|
||||
title: meta.title,
|
||||
icon: meta.icon,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
|
||||
grouped.get(groupIndex)!.children!.push({
|
||||
index: absolutePath,
|
||||
path: absolutePath,
|
||||
title: getRouteTitle(routeRecord),
|
||||
icon: resolveIcon(routeRecord.meta.icon, meta.icon),
|
||||
})
|
||||
}
|
||||
|
||||
return [...topLevelItems, ...grouped.values()]
|
||||
})
|
||||
|
||||
const defaultOpeneds = computed(() => {
|
||||
const openGroups = new Set<string>()
|
||||
for (const item of menuItems.value) {
|
||||
if (item.children?.some((child) => child.path && isActive(child.path))) {
|
||||
openGroups.add(item.index)
|
||||
}
|
||||
}
|
||||
return [...openGroups]
|
||||
})
|
||||
|
||||
function isActive(path: string): boolean {
|
||||
return route.path === path || route.path.startsWith(path + '/')
|
||||
return route.path === path || route.path.startsWith(`${path}/`)
|
||||
}
|
||||
|
||||
function navigateTo(path: string): void {
|
||||
function navigateTo(path?: string): void {
|
||||
if (!path || route.path === path) {
|
||||
return
|
||||
}
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
@@ -44,23 +149,45 @@ function navigateTo(path: string): void {
|
||||
<el-menu
|
||||
:collapse="isCollapsed"
|
||||
:default-active="route.path"
|
||||
:default-openeds="defaultOpeneds"
|
||||
background-color="#001529"
|
||||
text-color="rgba(255,255,255,0.65)"
|
||||
active-text-color="#fff"
|
||||
class="sidebar-menu"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
:index="item.path"
|
||||
@click="navigateTo(item.path)"
|
||||
:class="{ 'is-active': isActive(item.path) }"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
<template v-for="item in menuItems" :key="item.index">
|
||||
<el-sub-menu v-if="item.children?.length" :index="item.index">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in item.children"
|
||||
:key="child.index"
|
||||
:index="child.index"
|
||||
:class="{ 'is-active': child.path ? isActive(child.path) : false }"
|
||||
@click="navigateTo(child.path)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="child.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ child.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item
|
||||
v-else
|
||||
:index="item.index"
|
||||
:class="{ 'is-active': item.path ? isActive(item.path) : false }"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,6 +43,28 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'ops/dashboard',
|
||||
name: 'OpsDashboard',
|
||||
component: () => import('@/views/ops/dashboard/index.vue'),
|
||||
meta: {
|
||||
title: '运营看板',
|
||||
icon: 'DataAnalysis',
|
||||
requiresAuth: true,
|
||||
authority: 'system:ops:dashboard',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'billing/usage',
|
||||
name: 'BillingUsage',
|
||||
component: () => import('@/views/billing/usage/index.vue'),
|
||||
meta: {
|
||||
title: '用量计费',
|
||||
icon: 'Coin',
|
||||
requiresAuth: true,
|
||||
authority: 'system:billing:list',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/user',
|
||||
name: 'SystemUser',
|
||||
@@ -73,6 +95,26 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/tenant/lifecycle',
|
||||
name: 'SystemTenantLifecycle',
|
||||
component: () => import('@/views/system/tenant/lifecycle.vue'),
|
||||
meta: {
|
||||
title: '租户生命周期',
|
||||
icon: 'Clock',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/tenant-package',
|
||||
name: 'SystemTenantPackage',
|
||||
component: () => import('@/views/system/tenantPackage/index.vue'),
|
||||
meta: {
|
||||
title: '租户套餐',
|
||||
icon: 'Box',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/dict',
|
||||
name: 'SystemDict',
|
||||
@@ -93,6 +135,47 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/ai-config',
|
||||
name: 'SystemAiConfig',
|
||||
component: () => import('@/views/system/ai-config/index.vue'),
|
||||
meta: {
|
||||
title: 'AI 配置',
|
||||
icon: 'MagicStick',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/custom-field',
|
||||
name: 'SystemCustomField',
|
||||
component: () => import('@/views/system/custom-field/index.vue'),
|
||||
meta: {
|
||||
title: '自定义字段',
|
||||
icon: 'EditPen',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/help',
|
||||
name: 'SystemHelp',
|
||||
component: () => import('@/views/system/help/index.vue'),
|
||||
meta: {
|
||||
title: '帮助文档',
|
||||
icon: 'QuestionFilled',
|
||||
requiresAuth: true,
|
||||
authority: 'system:help:list',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/import-export',
|
||||
name: 'SystemImportExport',
|
||||
component: () => import('@/views/system/import-export/index.vue'),
|
||||
meta: {
|
||||
title: '导入导出',
|
||||
icon: 'UploadFilled',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/job',
|
||||
name: 'SystemJob',
|
||||
@@ -123,6 +206,16 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/post',
|
||||
name: 'SystemPost',
|
||||
component: () => import('@/views/system/post/index.vue'),
|
||||
meta: {
|
||||
title: '岗位管理',
|
||||
icon: 'Suitcase',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'monitor/operlog',
|
||||
name: 'MonitorOperlog',
|
||||
@@ -143,6 +236,27 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'monitor/server',
|
||||
name: 'MonitorServer',
|
||||
component: () => import('@/views/monitor/server/index.vue'),
|
||||
meta: {
|
||||
title: '服务监控',
|
||||
icon: 'Cpu',
|
||||
requiresAuth: true,
|
||||
authority: 'system:monitor:server',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'monitor/ai-assistant',
|
||||
name: 'MonitorAiAssistant',
|
||||
component: () => import('@/views/monitor/ai-assistant/index.vue'),
|
||||
meta: {
|
||||
title: 'AI 助手',
|
||||
icon: 'ChatDotRound',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/dept',
|
||||
name: 'SystemDept',
|
||||
@@ -163,6 +277,26 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/open-api/app',
|
||||
name: 'SystemOpenApiApp',
|
||||
component: () => import('@/views/system/open-api/app/index.vue'),
|
||||
meta: {
|
||||
title: '开放应用',
|
||||
icon: 'Link',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/open-api/secret',
|
||||
name: 'SystemOpenApiSecret',
|
||||
component: () => import('@/views/system/open-api/secret/index.vue'),
|
||||
meta: {
|
||||
title: '应用密钥',
|
||||
icon: 'Key',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/profile',
|
||||
name: 'SystemProfile',
|
||||
@@ -173,6 +307,46 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/webhook',
|
||||
name: 'SystemWebhook',
|
||||
component: () => import('@/views/system/webhook/index.vue'),
|
||||
meta: {
|
||||
title: 'Webhook',
|
||||
icon: 'Connection',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/task-log',
|
||||
name: 'SystemTaskLog',
|
||||
component: () => import('@/views/system/task-log/index.vue'),
|
||||
meta: {
|
||||
title: '任务执行日志',
|
||||
icon: 'List',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/message/inbox',
|
||||
name: 'SystemMessageInbox',
|
||||
component: () => import('@/views/system/message/inbox.vue'),
|
||||
meta: {
|
||||
title: '站内信收件箱',
|
||||
icon: 'MessageBox',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system/message/records',
|
||||
name: 'SystemMessageRecords',
|
||||
component: () => import('@/views/system/message/records.vue'),
|
||||
meta: {
|
||||
title: '消息发送记录',
|
||||
icon: 'Tickets',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'monitor/online-user',
|
||||
name: 'MonitorOnlineUser',
|
||||
@@ -194,6 +368,106 @@ const routes: RouteRecordRaw[] = [
|
||||
authority: 'system:audit:list',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'form/designer',
|
||||
name: 'FormDesigner',
|
||||
component: () => import('@/views/form/designer/index.vue'),
|
||||
meta: {
|
||||
title: '表单设计器',
|
||||
icon: 'EditPen',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'form/data',
|
||||
name: 'FormData',
|
||||
component: () => import('@/views/form/data/index.vue'),
|
||||
meta: {
|
||||
title: '表单数据',
|
||||
icon: 'Document',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'message/template',
|
||||
name: 'MessageTemplate',
|
||||
component: () => import('@/views/message/template/index.vue'),
|
||||
meta: {
|
||||
title: '消息模板',
|
||||
icon: 'ChatLineSquare',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'message/subscription',
|
||||
name: 'MessageSubscription',
|
||||
component: () => import('@/views/message/subscription/index.vue'),
|
||||
meta: {
|
||||
title: '消息订阅',
|
||||
icon: 'BellFilled',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'workflow/definition',
|
||||
name: 'WorkflowDefinition',
|
||||
component: () => import('@/views/workflow/definition/index.vue'),
|
||||
meta: {
|
||||
title: '流程定义',
|
||||
icon: 'Connection',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'workflow/instance',
|
||||
name: 'WorkflowInstance',
|
||||
component: () => import('@/views/workflow/instance/index.vue'),
|
||||
meta: {
|
||||
title: '流程实例',
|
||||
icon: 'Files',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'workflow/task',
|
||||
name: 'WorkflowTask',
|
||||
component: () => import('@/views/workflow/task/index.vue'),
|
||||
meta: {
|
||||
title: '流程任务',
|
||||
icon: 'Checked',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'report/designer',
|
||||
name: 'ReportDesigner',
|
||||
component: () => import('@/views/report/designer/index.vue'),
|
||||
meta: {
|
||||
title: '报表设计',
|
||||
icon: 'Histogram',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'report/preview',
|
||||
name: 'ReportPreview',
|
||||
component: () => import('@/views/report/preview/index.vue'),
|
||||
meta: {
|
||||
title: '报表预览',
|
||||
icon: 'View',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tenant/brand',
|
||||
name: 'TenantBrand',
|
||||
component: () => import('@/views/tenant/brand/index.vue'),
|
||||
meta: {
|
||||
title: '租户品牌',
|
||||
icon: 'Brush',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const backendUrl = env.VITE_BACKEND_URL || 'http://localhost:8080'
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: backendUrl,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.ljwx.platform.app.ai.tool;
|
||||
|
||||
import com.ljwx.platform.app.domain.entity.SysJob;
|
||||
import com.ljwx.platform.app.domain.entity.TaskExecutionLog;
|
||||
import com.ljwx.platform.app.infra.mapper.SysJobMapper;
|
||||
import com.ljwx.platform.app.infra.mapper.TaskExecutionLogMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -18,6 +22,9 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class JobQueryTool {
|
||||
|
||||
private final SysJobMapper sysJobMapper;
|
||||
private final TaskExecutionLogMapper taskExecutionLogMapper;
|
||||
|
||||
/**
|
||||
* 列出定时任务及状态
|
||||
* 权限约束:仅 system:job:list
|
||||
@@ -25,12 +32,20 @@ public class JobQueryTool {
|
||||
* @return 定时任务列表
|
||||
*/
|
||||
public Map<String, Object> listScheduledJobs() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应调用 jobMapper 查询
|
||||
result.put("total", 0);
|
||||
result.put("jobs", List.of());
|
||||
return result;
|
||||
List<SysJob> jobs = sysJobMapper.selectList(new SysJob());
|
||||
List<Map<String, Object>> jobSummaries = jobs.stream()
|
||||
.map(job -> Map.<String, Object>of(
|
||||
"id", job.getId(),
|
||||
"jobName", job.getJobName(),
|
||||
"jobGroup", job.getJobGroup() == null ? "" : job.getJobGroup(),
|
||||
"cronExpression", job.getCronExpression() == null ? "" : job.getCronExpression(),
|
||||
"status", job.getStatus() == null ? 0 : job.getStatus(),
|
||||
"description", job.getDescription() == null ? "" : job.getDescription()))
|
||||
.toList();
|
||||
return Map.of(
|
||||
"total", jobSummaries.size(),
|
||||
"jobs", jobSummaries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,11 +57,38 @@ public class JobQueryTool {
|
||||
* @return 任务执行日志
|
||||
*/
|
||||
public Map<String, Object> getJobLogs(Long jobId, Integer limit) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应调用 jobLogMapper 查询
|
||||
result.put("total", 0);
|
||||
result.put("logs", List.of());
|
||||
return result;
|
||||
if (jobId == null) {
|
||||
return Map.of("total", 0, "logs", List.of());
|
||||
}
|
||||
|
||||
SysJob job = sysJobMapper.selectById(jobId);
|
||||
if (job == null) {
|
||||
return Map.of("total", 0, "logs", List.of(), "message", "任务不存在");
|
||||
}
|
||||
|
||||
TaskExecutionLog query = new TaskExecutionLog();
|
||||
query.setTaskName(job.getJobName());
|
||||
query.setTaskGroup(job.getJobGroup());
|
||||
List<TaskExecutionLog> logs = taskExecutionLogMapper.selectList(query);
|
||||
int max = limit == null || limit <= 0 ? 10 : limit;
|
||||
|
||||
List<Map<String, Object>> logSummaries = new ArrayList<>();
|
||||
for (TaskExecutionLog log : logs.stream().limit(max).toList()) {
|
||||
logSummaries.add(Map.of(
|
||||
"id", log.getId(),
|
||||
"taskName", log.getTaskName(),
|
||||
"taskGroup", log.getTaskGroup() == null ? "" : log.getTaskGroup(),
|
||||
"status", log.getStatus() == null ? "" : log.getStatus(),
|
||||
"startTime", log.getStartTime(),
|
||||
"endTime", log.getEndTime(),
|
||||
"duration", log.getDuration() == null ? 0 : log.getDuration(),
|
||||
"errorMessage", log.getErrorMessage() == null ? "" : log.getErrorMessage()
|
||||
));
|
||||
}
|
||||
|
||||
return Map.of(
|
||||
"total", logs.size(),
|
||||
"logs", logSummaries
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.ljwx.platform.app.ai.tool;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -19,6 +21,8 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class LogQueryTool {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 查询操作日志
|
||||
* 权限约束:仅 system:operlog:list
|
||||
@@ -32,12 +36,56 @@ public class LogQueryTool {
|
||||
*/
|
||||
public Map<String, Object> queryOperationLogs(LocalDateTime startTime, LocalDateTime endTime,
|
||||
Long userId, String module, Integer limit) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应调用 operationLogMapper 查询
|
||||
result.put("total", 0);
|
||||
result.put("logs", List.of());
|
||||
return result;
|
||||
StringBuilder where = new StringBuilder(" WHERE deleted = FALSE");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (startTime != null) {
|
||||
where.append(" AND created_time >= ?");
|
||||
params.add(Timestamp.valueOf(startTime));
|
||||
}
|
||||
if (endTime != null) {
|
||||
where.append(" AND created_time <= ?");
|
||||
params.add(Timestamp.valueOf(endTime));
|
||||
}
|
||||
if (userId != null) {
|
||||
where.append(" AND operator_id = ?");
|
||||
params.add(userId);
|
||||
}
|
||||
if (module != null && !module.isBlank()) {
|
||||
where.append(" AND title ILIKE ?");
|
||||
params.add("%" + module.trim() + "%");
|
||||
}
|
||||
|
||||
Long total = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM sys_operation_log" + where,
|
||||
params.toArray(),
|
||||
Long.class
|
||||
);
|
||||
|
||||
int max = limit == null || limit <= 0 ? 10 : limit;
|
||||
List<Object> logParams = new ArrayList<>(params);
|
||||
logParams.add(max);
|
||||
List<Map<String, Object>> logs = jdbcTemplate.query(
|
||||
"SELECT id, title, operator_id, operator_name, status, request_method, request_url, cost_time, created_time " +
|
||||
"FROM sys_operation_log" + where + " ORDER BY created_time DESC LIMIT ?",
|
||||
logParams.toArray(),
|
||||
(rs, rowNum) -> Map.<String, Object>of(
|
||||
"id", rs.getLong("id"),
|
||||
"title", rs.getString("title") == null ? "" : rs.getString("title"),
|
||||
"operatorId", rs.getLong("operator_id"),
|
||||
"operatorName", rs.getString("operator_name") == null ? "" : rs.getString("operator_name"),
|
||||
"status", rs.getInt("status"),
|
||||
"requestMethod", rs.getString("request_method") == null ? "" : rs.getString("request_method"),
|
||||
"requestUrl", rs.getString("request_url") == null ? "" : rs.getString("request_url"),
|
||||
"costTime", rs.getLong("cost_time"),
|
||||
"createdTime", rs.getTimestamp("created_time").toLocalDateTime()
|
||||
)
|
||||
);
|
||||
|
||||
return Map.of(
|
||||
"total", total == null ? 0L : total,
|
||||
"logs", logs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +100,49 @@ public class LogQueryTool {
|
||||
*/
|
||||
public Map<String, Object> queryLoginLogs(LocalDateTime startTime, LocalDateTime endTime,
|
||||
Long userId, Integer limit) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应调用 loginLogMapper 查询
|
||||
result.put("total", 0);
|
||||
result.put("logs", List.of());
|
||||
return result;
|
||||
String fromClause = " FROM sys_login_log l LEFT JOIN sys_user u ON u.username = l.username AND u.deleted = FALSE";
|
||||
StringBuilder where = new StringBuilder(" WHERE l.deleted = FALSE");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (startTime != null) {
|
||||
where.append(" AND l.login_time >= ?");
|
||||
params.add(Timestamp.valueOf(startTime));
|
||||
}
|
||||
if (endTime != null) {
|
||||
where.append(" AND l.login_time <= ?");
|
||||
params.add(Timestamp.valueOf(endTime));
|
||||
}
|
||||
if (userId != null) {
|
||||
where.append(" AND u.id = ?");
|
||||
params.add(userId);
|
||||
}
|
||||
|
||||
Long total = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*)" + fromClause + where,
|
||||
params.toArray(),
|
||||
Long.class
|
||||
);
|
||||
|
||||
int max = limit == null || limit <= 0 ? 10 : limit;
|
||||
List<Object> logParams = new ArrayList<>(params);
|
||||
logParams.add(max);
|
||||
List<Map<String, Object>> logs = jdbcTemplate.query(
|
||||
"SELECT l.id, l.username, l.status, l.message, l.ip_address, l.login_time" +
|
||||
fromClause + where + " ORDER BY l.login_time DESC LIMIT ?",
|
||||
logParams.toArray(),
|
||||
(rs, rowNum) -> Map.<String, Object>of(
|
||||
"id", rs.getLong("id"),
|
||||
"username", rs.getString("username") == null ? "" : rs.getString("username"),
|
||||
"status", rs.getInt("status"),
|
||||
"message", rs.getString("message") == null ? "" : rs.getString("message"),
|
||||
"ipAddress", rs.getString("ip_address") == null ? "" : rs.getString("ip_address"),
|
||||
"loginTime", rs.getTimestamp("login_time").toLocalDateTime()
|
||||
)
|
||||
);
|
||||
|
||||
return Map.of(
|
||||
"total", total == null ? 0L : total,
|
||||
"logs", logs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.ljwx.platform.app.ai.tool;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.stats.CacheStats;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
@@ -20,6 +24,8 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class MonitorTool {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 获取服务器状态
|
||||
* 权限约束:仅 system:monitor:server
|
||||
@@ -69,13 +75,30 @@ public class MonitorTool {
|
||||
*/
|
||||
public Map<String, Object> getCacheStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应从 CacheManager 获取 Caffeine 缓存统计
|
||||
stats.put("hitRate", 0.85);
|
||||
stats.put("missRate", 0.15);
|
||||
stats.put("totalRequests", 10000);
|
||||
stats.put("cacheSize", 500);
|
||||
long totalRequests = 0L;
|
||||
long totalHits = 0L;
|
||||
long totalMisses = 0L;
|
||||
long cacheSize = 0L;
|
||||
|
||||
for (String cacheName : cacheManager.getCacheNames()) {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache instanceof CaffeineCache caffeineCache) {
|
||||
cacheSize += caffeineCache.getNativeCache().estimatedSize();
|
||||
CacheStats cacheStats = caffeineCache.getNativeCache().stats();
|
||||
totalRequests += cacheStats.requestCount();
|
||||
totalHits += cacheStats.hitCount();
|
||||
totalMisses += cacheStats.missCount();
|
||||
}
|
||||
}
|
||||
|
||||
double hitRate = totalRequests == 0 ? 0D : (double) totalHits / totalRequests;
|
||||
double missRate = totalRequests == 0 ? 0D : (double) totalMisses / totalRequests;
|
||||
|
||||
stats.put("hitRate", hitRate);
|
||||
stats.put("missRate", missRate);
|
||||
stats.put("totalRequests", totalRequests);
|
||||
stats.put("cacheSize", cacheSize);
|
||||
stats.put("cacheCount", cacheManager.getCacheNames().size());
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.ljwx.platform.app.ai.tool;
|
||||
|
||||
import com.ljwx.platform.app.appservice.OnlineUserAppService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,8 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class OnlineUserTool {
|
||||
|
||||
private final OnlineUserAppService onlineUserAppService;
|
||||
|
||||
/**
|
||||
* 获取当前在线用户数
|
||||
* 权限约束:仅 system:online:list
|
||||
@@ -24,11 +27,11 @@ public class OnlineUserTool {
|
||||
* @return 在线用户数
|
||||
*/
|
||||
public Map<String, Object> getOnlineUserCount() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 简化实现:返回占位数据
|
||||
// 实际应从 Redis 或 Session 管理器获取
|
||||
result.put("onlineCount", 0);
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
return result;
|
||||
List<Map<String, String>> onlineUsers = onlineUserAppService.listOnlineUsers();
|
||||
return Map.of(
|
||||
"onlineCount", onlineUsers.size(),
|
||||
"users", onlineUsers.stream().limit(10).toList(),
|
||||
"timestamp", System.currentTimeMillis()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ public class AuthAppService {
|
||||
private final CurrentTenantHolder tenantHolder;
|
||||
private final TokenBlacklistService tokenBlacklistService;
|
||||
private final LoginLockoutService loginLockoutService;
|
||||
private final LoginLogAppService loginLogAppService;
|
||||
private final OnlineUserAppService onlineUserAppService;
|
||||
|
||||
/**
|
||||
* 用户登录:校验密码 → 加载权限 → 签发双 Token。
|
||||
@@ -52,8 +54,19 @@ public class AuthAppService {
|
||||
* selectByUsername 跨租户查询(LIMIT 1 取首条)。
|
||||
*/
|
||||
public LoginVO login(LoginDTO dto) {
|
||||
return login(dto, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录:校验密码 → 加载权限 → 签发双 Token。
|
||||
*
|
||||
* <p>登录时无 Auth 上下文,TenantLineInterceptor 绕过,
|
||||
* selectByUsername 跨租户查询(LIMIT 1 取首条)。
|
||||
*/
|
||||
public LoginVO login(LoginDTO dto, String clientIp, String userAgent) {
|
||||
// 0. 检查账号是否被锁定
|
||||
if (loginLockoutService.isLocked(dto.getUsername())) {
|
||||
recordLogin(dto.getUsername(), clientIp, userAgent, false, "账号已锁定,请30分钟后重试");
|
||||
throw new BusinessException(ErrorCode.ACCOUNT_LOCKED, "账号已锁定,请30分钟后重试");
|
||||
}
|
||||
|
||||
@@ -61,17 +74,20 @@ public class AuthAppService {
|
||||
SysUser user = userMapper.selectByUsername(dto.getUsername());
|
||||
if (user == null) {
|
||||
loginLockoutService.recordFailure(dto.getUsername());
|
||||
recordLogin(dto.getUsername(), clientIp, userAgent, false, "用户名或密码错误");
|
||||
throw new BusinessException(ErrorCode.TOKEN_INVALID, "用户名或密码错误");
|
||||
}
|
||||
|
||||
// 2. BCrypt 密码校验(日志不输出 password)
|
||||
if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
|
||||
loginLockoutService.recordFailure(dto.getUsername());
|
||||
recordLogin(dto.getUsername(), clientIp, userAgent, false, "用户名或密码错误");
|
||||
throw new BusinessException(ErrorCode.TOKEN_INVALID, "用户名或密码错误");
|
||||
}
|
||||
|
||||
// 3. 账号状态校验
|
||||
if (user.getStatus() == null || user.getStatus() != 1) {
|
||||
recordLogin(dto.getUsername(), clientIp, userAgent, false, "账号已禁用");
|
||||
throw new BusinessException(ErrorCode.TOKEN_INVALID, "账号已禁用");
|
||||
}
|
||||
|
||||
@@ -96,6 +112,8 @@ public class AuthAppService {
|
||||
vo.setRefreshToken(refreshToken);
|
||||
vo.setExpiresIn(jwtProperties.getAccessTokenExpiration());
|
||||
vo.setUserInfo(userInfo);
|
||||
registerOnlineToken(accessToken, user.getUsername());
|
||||
recordLogin(dto.getUsername(), clientIp, userAgent, true, "登录成功");
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -125,6 +143,7 @@ public class AuthAppService {
|
||||
vo.setAccessToken(newAccess);
|
||||
vo.setRefreshToken(newRefresh);
|
||||
vo.setExpiresIn(jwtProperties.getAccessTokenExpiration());
|
||||
registerOnlineToken(newAccess, username);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -172,9 +191,23 @@ public class AuthAppService {
|
||||
long remainingSeconds =
|
||||
(claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000;
|
||||
tokenBlacklistService.addToBlacklist(jti, remainingSeconds);
|
||||
onlineUserAppService.remove(jti);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerOnlineToken(String token, String username) {
|
||||
Claims claims = jwtTokenProvider.parseToken(token);
|
||||
String jti = jwtTokenProvider.getJti(claims);
|
||||
if (jti != null) {
|
||||
onlineUserAppService.register(jti, username);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordLogin(String username, String clientIp, String userAgent,
|
||||
boolean success, String message) {
|
||||
loginLogAppService.recordLogin(username, clientIp, userAgent, success ? 1 : 0, message);
|
||||
}
|
||||
|
||||
private UserInfoVO buildUserInfo(SysUser user, List<String> authorities, List<String> roles) {
|
||||
UserInfoVO vo = new UserInfoVO();
|
||||
vo.setId(user.getId());
|
||||
|
||||
+24
-2
@@ -6,7 +6,9 @@ import com.ljwx.platform.app.domain.dto.ConfigUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.entity.SysConfig;
|
||||
import com.ljwx.platform.app.infra.mapper.SysConfigMapper;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
@@ -39,6 +41,14 @@ public class ConfigAppService {
|
||||
return new PageResult<>(records, total);
|
||||
}
|
||||
|
||||
public SysConfig getConfigById(Long id) {
|
||||
SysConfig config = configMapper.selectById(id);
|
||||
if (config == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "配置不存在");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按键名查询配置值,使用 Caffeine 缓存(TTL = 10 min)。
|
||||
*
|
||||
@@ -74,9 +84,9 @@ public class ConfigAppService {
|
||||
* 更新系统配置,并清除对应配置缓存。
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "sysConfig", key = "#dto.configKey")
|
||||
@CacheEvict(cacheNames = "sysConfig", allEntries = true)
|
||||
public void updateConfig(ConfigUpdateDTO dto) {
|
||||
SysConfig existing = configMapper.selectById(dto.getId());
|
||||
SysConfig existing = getConfigById(dto.getId());
|
||||
existing.setConfigName(dto.getConfigName());
|
||||
existing.setConfigKey(dto.getConfigKey());
|
||||
existing.setConfigValue(dto.getConfigValue());
|
||||
@@ -87,4 +97,16 @@ public class ConfigAppService {
|
||||
existing.setVersion(dto.getVersion());
|
||||
configMapper.updateById(existing);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "sysConfig", allEntries = true)
|
||||
public void deleteConfig(Long id) {
|
||||
getConfigById(id);
|
||||
configMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "sysConfig", allEntries = true)
|
||||
public void refreshConfigs() {
|
||||
// Cache eviction is handled by the annotation.
|
||||
}
|
||||
}
|
||||
|
||||
+75
-2
@@ -1,6 +1,8 @@
|
||||
package com.ljwx.platform.app.appservice;
|
||||
|
||||
import com.ljwx.platform.app.domain.dto.DictCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictDataCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictDataUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictQueryDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.entity.SysDictData;
|
||||
@@ -8,7 +10,9 @@ import com.ljwx.platform.app.domain.entity.SysDictType;
|
||||
import com.ljwx.platform.app.infra.mapper.SysDictDataMapper;
|
||||
import com.ljwx.platform.app.infra.mapper.SysDictTypeMapper;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
@@ -42,6 +46,14 @@ public class DictAppService {
|
||||
return new PageResult<>(records, total);
|
||||
}
|
||||
|
||||
public SysDictType getDictTypeById(Long id) {
|
||||
SysDictType dictType = dictTypeMapper.selectById(id);
|
||||
if (dictType == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "字典类型不存在");
|
||||
}
|
||||
return dictType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按字典类型查询字典数据项,使用 Caffeine 缓存(TTL = 10 min)。
|
||||
*
|
||||
@@ -58,6 +70,7 @@ public class DictAppService {
|
||||
* tenant_id 由 TenantLineInterceptor(MyBatis Interceptor)自动注入,Service 层禁止手动设置。
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public Long createDictType(DictCreateDTO dto) {
|
||||
long id = idGenerator.nextId();
|
||||
|
||||
@@ -76,9 +89,9 @@ public class DictAppService {
|
||||
* 更新字典类型,并清除对应字典数据缓存。
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", key = "#dto.dictType")
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public void updateDictType(DictUpdateDTO dto) {
|
||||
SysDictType existing = dictTypeMapper.selectById(dto.getId());
|
||||
SysDictType existing = getDictTypeById(dto.getId());
|
||||
existing.setDictName(dto.getDictName());
|
||||
existing.setDictType(dto.getDictType());
|
||||
if (dto.getStatus() != null) {
|
||||
@@ -88,4 +101,64 @@ public class DictAppService {
|
||||
existing.setVersion(dto.getVersion());
|
||||
dictTypeMapper.updateById(existing);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public void deleteDictType(Long id) {
|
||||
getDictTypeById(id);
|
||||
dictTypeMapper.deleteById(id);
|
||||
}
|
||||
|
||||
public SysDictData getDictDataById(Long id) {
|
||||
SysDictData dictData = dictDataMapper.selectById(id);
|
||||
if (dictData == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "字典数据不存在");
|
||||
}
|
||||
return dictData;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public Long createDictData(DictDataCreateDTO dto) {
|
||||
long id = idGenerator.nextId();
|
||||
|
||||
SysDictData dictData = new SysDictData();
|
||||
dictData.setId(id);
|
||||
dictData.setDictType(dto.getDictType());
|
||||
dictData.setDictLabel(dto.getDictLabel());
|
||||
dictData.setDictValue(dto.getDictValue());
|
||||
dictData.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
|
||||
dictData.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
|
||||
dictData.setCssClass(dto.getCssClass());
|
||||
dictData.setListClass(dto.getListClass());
|
||||
dictData.setIsDefault(dto.getIsDefault() != null ? dto.getIsDefault() : Boolean.FALSE);
|
||||
dictData.setRemark(dto.getRemark());
|
||||
|
||||
dictDataMapper.insert(dictData);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public void updateDictData(DictDataUpdateDTO dto) {
|
||||
SysDictData existing = getDictDataById(dto.getId());
|
||||
existing.setDictType(dto.getDictType());
|
||||
existing.setDictLabel(dto.getDictLabel());
|
||||
existing.setDictValue(dto.getDictValue());
|
||||
existing.setSortOrder(dto.getSortOrder());
|
||||
existing.setStatus(dto.getStatus());
|
||||
existing.setCssClass(dto.getCssClass());
|
||||
existing.setListClass(dto.getListClass());
|
||||
existing.setIsDefault(dto.getIsDefault());
|
||||
existing.setRemark(dto.getRemark());
|
||||
existing.setVersion(dto.getVersion());
|
||||
dictDataMapper.updateById(existing);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = "dictData", allEntries = true)
|
||||
public void deleteDictData(Long id) {
|
||||
getDictDataById(id);
|
||||
dictDataMapper.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -8,7 +8,9 @@ import com.ljwx.platform.app.infra.mapper.SysJobMapper;
|
||||
import com.ljwx.platform.app.infra.quartz.QuartzJobDispatcher;
|
||||
import com.ljwx.platform.core.context.CurrentTenantHolder;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.quartz.CronScheduleBuilder;
|
||||
import org.quartz.CronTrigger;
|
||||
@@ -48,6 +50,14 @@ public class JobAppService {
|
||||
return new PageResult<>(records, total);
|
||||
}
|
||||
|
||||
public SysJob getJobById(Long id) {
|
||||
SysJob job = jobMapper.selectById(id);
|
||||
if (job == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "任务不存在");
|
||||
}
|
||||
return job;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long createJob(JobCreateDTO dto) throws SchedulerException {
|
||||
long id = idGenerator.nextId();
|
||||
@@ -82,7 +92,7 @@ public class JobAppService {
|
||||
public void updateJob(JobUpdateDTO dto) throws SchedulerException {
|
||||
Long tenantId = tenantHolder.getTenantId();
|
||||
|
||||
SysJob existing = jobMapper.selectById(dto.getId());
|
||||
SysJob existing = getJobById(dto.getId());
|
||||
existing.setJobName(dto.getJobName());
|
||||
if (dto.getJobGroup() != null) {
|
||||
existing.setJobGroup(dto.getJobGroup());
|
||||
@@ -114,7 +124,7 @@ public class JobAppService {
|
||||
JobKey jobKey = new JobKey(String.valueOf(id), "TENANT_" + tenantId);
|
||||
scheduler.pauseJob(jobKey);
|
||||
|
||||
SysJob job = jobMapper.selectById(id);
|
||||
SysJob job = getJobById(id);
|
||||
job.setStatus(0);
|
||||
jobMapper.updateById(job);
|
||||
}
|
||||
@@ -125,8 +135,16 @@ public class JobAppService {
|
||||
JobKey jobKey = new JobKey(String.valueOf(id), "TENANT_" + tenantId);
|
||||
scheduler.resumeJob(jobKey);
|
||||
|
||||
SysJob job = jobMapper.selectById(id);
|
||||
SysJob job = getJobById(id);
|
||||
job.setStatus(1);
|
||||
jobMapper.updateById(job);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteJob(Long id) throws SchedulerException {
|
||||
Long tenantId = tenantHolder.getTenantId();
|
||||
getJobById(id);
|
||||
scheduler.deleteJob(new JobKey(String.valueOf(id), "TENANT_" + tenantId));
|
||||
jobMapper.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -46,6 +46,17 @@ public class NoticeAppService {
|
||||
return new PageResult<>(records, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询通知详情。
|
||||
*/
|
||||
public SysNotice getNotice(Long id) {
|
||||
SysNotice notice = noticeMapper.selectById(id);
|
||||
if (notice == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "通知不存在");
|
||||
}
|
||||
return notice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知/公告。
|
||||
* tenant_id 由 TenantLineInterceptor 自动注入,禁止手动设置。
|
||||
@@ -142,4 +153,16 @@ public class NoticeAppService {
|
||||
if (userId == null) return 0L;
|
||||
return noticeUserMapper.countUnread(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知。
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteNotice(Long id) {
|
||||
SysNotice notice = noticeMapper.selectById(id);
|
||||
if (notice == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "通知不存在");
|
||||
}
|
||||
noticeMapper.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -4,10 +4,13 @@ import com.ljwx.platform.app.domain.dto.OperationLogQueryDTO;
|
||||
import com.ljwx.platform.app.domain.entity.SysOperationLog;
|
||||
import com.ljwx.platform.app.infra.mapper.SysOperationLogMapper;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -76,6 +79,14 @@ public class OperationLogAppService {
|
||||
return new PageResult<>(records, total);
|
||||
}
|
||||
|
||||
public SysOperationLog getOperationLogById(Long id) {
|
||||
SysOperationLog log = operationLogMapper.selectById(id);
|
||||
if (log == null) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "操作日志不存在");
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出操作日志(返回列表,不设页大小限制;调用方可进一步处理为文件)。
|
||||
*/
|
||||
@@ -85,6 +96,17 @@ public class OperationLogAppService {
|
||||
return operationLogMapper.selectList(query);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteOperationLog(Long id) {
|
||||
getOperationLogById(id);
|
||||
operationLogMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int cleanOperationLogs() {
|
||||
return operationLogMapper.cleanAll();
|
||||
}
|
||||
|
||||
// ── 私有工具方法 ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package com.ljwx.platform.app.appservice;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ljwx.platform.app.domain.Tenant;
|
||||
import com.ljwx.platform.app.domain.dto.TenantCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.TenantQueryDTO;
|
||||
import com.ljwx.platform.app.domain.dto.TenantUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.vo.TenantVO;
|
||||
import com.ljwx.platform.app.mapper.TenantMapper;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 租户管理应用服务。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TenantAppService {
|
||||
|
||||
private final TenantMapper tenantMapper;
|
||||
private final SnowflakeIdGenerator idGenerator;
|
||||
|
||||
public PageResult<TenantVO> listTenants(TenantQueryDTO query) {
|
||||
LambdaQueryWrapper<Tenant> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Tenant::getDeleted, false);
|
||||
|
||||
if (StringUtils.hasText(query.getName())) {
|
||||
wrapper.like(Tenant::getName, query.getName().trim());
|
||||
}
|
||||
if (StringUtils.hasText(query.getCode())) {
|
||||
wrapper.like(Tenant::getCode, query.getCode().trim());
|
||||
}
|
||||
if (query.getStatus() != null) {
|
||||
wrapper.eq(Tenant::getStatus, query.getStatus());
|
||||
}
|
||||
wrapper.orderByDesc(Tenant::getCreatedTime);
|
||||
|
||||
Page<Tenant> page = new Page<>(query.getPageNum(), query.getPageSize());
|
||||
Page<Tenant> result = tenantMapper.selectPage(page, wrapper);
|
||||
List<TenantVO> rows = result.getRecords().stream()
|
||||
.map(this::toVO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageResult<>(rows, result.getTotal());
|
||||
}
|
||||
|
||||
public TenantVO getTenant(Long id) {
|
||||
Tenant tenant = requireTenant(id);
|
||||
return toVO(tenant);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long createTenant(TenantCreateDTO dto) {
|
||||
ensureCodeUnique(dto.getCode(), null);
|
||||
|
||||
Tenant tenant = new Tenant();
|
||||
tenant.setId(idGenerator.nextId());
|
||||
tenant.setName(dto.getName().trim());
|
||||
tenant.setCode(dto.getCode().trim());
|
||||
tenant.setStatus(1);
|
||||
tenant.setLifecycleStatus("ACTIVE");
|
||||
tenant.setTenantId(0L);
|
||||
tenant.setDeleted(false);
|
||||
tenant.setVersion(1);
|
||||
|
||||
tenantMapper.insert(tenant);
|
||||
return tenant.getId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateTenant(Long id, TenantUpdateDTO dto) {
|
||||
Tenant tenant = requireTenant(id);
|
||||
|
||||
if (StringUtils.hasText(dto.getCode())) {
|
||||
ensureCodeUnique(dto.getCode(), id);
|
||||
tenant.setCode(dto.getCode().trim());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getName())) {
|
||||
tenant.setName(dto.getName().trim());
|
||||
}
|
||||
if (dto.getStatus() != null) {
|
||||
tenant.setStatus(dto.getStatus());
|
||||
}
|
||||
|
||||
tenantMapper.updateById(tenant);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteTenant(Long id) {
|
||||
Tenant tenant = requireTenant(id);
|
||||
tenant.setDeleted(true);
|
||||
tenantMapper.updateById(tenant);
|
||||
}
|
||||
|
||||
private Tenant requireTenant(Long id) {
|
||||
Tenant tenant = tenantMapper.selectById(id);
|
||||
if (tenant == null || Boolean.TRUE.equals(tenant.getDeleted())) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "租户不存在");
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
|
||||
private void ensureCodeUnique(String code, Long excludeId) {
|
||||
LambdaQueryWrapper<Tenant> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Tenant::getCode, code.trim())
|
||||
.eq(Tenant::getDeleted, false);
|
||||
Tenant existing = tenantMapper.selectOne(wrapper);
|
||||
if (existing != null && (excludeId == null || !existing.getId().equals(excludeId))) {
|
||||
throw new BusinessException(ErrorCode.PARAM_VALIDATION_FAILED, "租户编码已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private TenantVO toVO(Tenant tenant) {
|
||||
TenantVO vo = new TenantVO();
|
||||
BeanUtils.copyProperties(tenant, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+46
-2
@@ -13,6 +13,13 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.InitialDirContext;
|
||||
import java.util.Hashtable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -150,8 +157,10 @@ public class TenantDomainAppService {
|
||||
throw new BusinessException(ErrorCode.DOMAIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
// BL-43-08: 域名验证(简化版,实际应查询 DNS TXT 记录)
|
||||
// 这里仅做标记,实际 DNS 验证需要集成 DNS 查询服务
|
||||
if (!hasMatchingVerifyRecord(domain.getDomain(), domain.getVerifyToken())) {
|
||||
throw new BusinessException(ErrorCode.DOMAIN_VERIFY_FAILED);
|
||||
}
|
||||
|
||||
domain.setVerified(true);
|
||||
domain.setVerifiedTime(LocalDateTime.now());
|
||||
tenantDomainMapper.updateById(domain);
|
||||
@@ -165,4 +174,39 @@ public class TenantDomainAppService {
|
||||
BeanUtils.copyProperties(domain, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private boolean hasMatchingVerifyRecord(String domain, String verifyToken) {
|
||||
String recordName = "_ljwx-verify." + domain;
|
||||
Hashtable<String, String> env = new Hashtable<>();
|
||||
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
|
||||
|
||||
DirContext context = null;
|
||||
try {
|
||||
context = new InitialDirContext(env);
|
||||
Attributes attributes = context.getAttributes(recordName, new String[]{"TXT"});
|
||||
Attribute txt = attributes.get("TXT");
|
||||
if (txt == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NamingEnumeration<?> values = txt.getAll();
|
||||
while (values.hasMore()) {
|
||||
String candidate = String.valueOf(values.next()).replace("\"", "").trim();
|
||||
if (verifyToken.equals(candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warn("Tenant domain TXT lookup failed for {}: {}", recordName, e.getMessage());
|
||||
throw new BusinessException(ErrorCode.DOMAIN_VERIFY_FAILED);
|
||||
} finally {
|
||||
if (context != null) {
|
||||
try {
|
||||
context.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -33,8 +33,11 @@ public class AuthController {
|
||||
|
||||
@PreAuthorize("permitAll()")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto) {
|
||||
return Result.ok(authAppService.login(dto));
|
||||
public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto, HttpServletRequest request) {
|
||||
return Result.ok(authAppService.login(
|
||||
dto,
|
||||
extractClientIp(request),
|
||||
request.getHeader("User-Agent")));
|
||||
}
|
||||
|
||||
@PreAuthorize("permitAll()")
|
||||
@@ -57,4 +60,12 @@ public class AuthController {
|
||||
authAppService.logout(token);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private String extractClientIp(HttpServletRequest request) {
|
||||
String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (forwardedFor != null && !forwardedFor.isBlank()) {
|
||||
return forwardedFor.split(",")[0].trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -10,6 +10,7 @@ import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -23,7 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 权限按 spec/03-api.md §Configs 路由定义。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/configs")
|
||||
@RequestMapping({"/api/v1/configs", "/api/configs"})
|
||||
@RequiredArgsConstructor
|
||||
public class ConfigController {
|
||||
|
||||
@@ -50,9 +51,29 @@ public class ConfigController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('config:read')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<SysConfig> detail(@PathVariable Long id) {
|
||||
return Result.ok(configAppService.getConfigById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('config:write')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
configAppService.deleteConfig(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('config:read')")
|
||||
@GetMapping("/key/{key}")
|
||||
public Result<SysConfig> getByKey(@PathVariable String key) {
|
||||
return Result.ok(configAppService.getConfigByKey(key));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('config:write')")
|
||||
@PostMapping("/refresh")
|
||||
public Result<Void> refresh() {
|
||||
configAppService.refreshConfigs();
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
+45
-7
@@ -2,6 +2,8 @@ package com.ljwx.platform.app.controller;
|
||||
|
||||
import com.ljwx.platform.app.appservice.DictAppService;
|
||||
import com.ljwx.platform.app.domain.dto.DictCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictDataCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictDataUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictQueryDTO;
|
||||
import com.ljwx.platform.app.domain.dto.DictUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.entity.SysDictData;
|
||||
@@ -10,6 +12,7 @@ import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -26,26 +29,32 @@ import java.util.List;
|
||||
* 权限按 spec/03-api.md §Dicts 路由定义。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/dicts")
|
||||
@RequestMapping({"/api/v1/dicts", "/api/dicts"})
|
||||
@RequiredArgsConstructor
|
||||
public class DictController {
|
||||
|
||||
private final DictAppService dictAppService;
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:read')")
|
||||
@GetMapping
|
||||
@GetMapping({"", "/types"})
|
||||
public Result<PageResult<SysDictType>> list(DictQueryDTO query) {
|
||||
return Result.ok(dictAppService.listDictTypes(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@PostMapping
|
||||
@PostMapping({"", "/types"})
|
||||
public Result<Long> create(@RequestBody @Valid DictCreateDTO dto) {
|
||||
return Result.ok(dictAppService.createDictType(dto));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:read')")
|
||||
@GetMapping({"/{id}", "/types/{id}"})
|
||||
public Result<SysDictType> detail(@PathVariable Long id) {
|
||||
return Result.ok(dictAppService.getDictTypeById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@PutMapping("/{id}")
|
||||
@PutMapping({"/{id}", "/types/{id}"})
|
||||
public Result<Void> update(@PathVariable Long id,
|
||||
@RequestBody @Valid DictUpdateDTO dto) {
|
||||
dto.setId(id);
|
||||
@@ -53,9 +62,38 @@ public class DictController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@DeleteMapping({"/{id}", "/types/{id}"})
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
dictAppService.deleteDictType(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:read')")
|
||||
@GetMapping("/type/{type}")
|
||||
public Result<List<SysDictData>> getDictDataByType(@PathVariable String type) {
|
||||
return Result.ok(dictAppService.getDictDataByType(type));
|
||||
@GetMapping({"/type/{dictType}", "/data/{dictType}"})
|
||||
public Result<List<SysDictData>> getDictDataByType(@PathVariable String dictType) {
|
||||
return Result.ok(dictAppService.getDictDataByType(dictType));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@PostMapping("/data")
|
||||
public Result<Long> createData(@RequestBody @Valid DictDataCreateDTO dto) {
|
||||
return Result.ok(dictAppService.createDictData(dto));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@PutMapping("/data/{id}")
|
||||
public Result<Void> updateData(@PathVariable Long id,
|
||||
@RequestBody @Valid DictDataUpdateDTO dto) {
|
||||
dto.setId(id);
|
||||
dictAppService.updateDictData(dto);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('dict:write')")
|
||||
@DeleteMapping("/data/{id}")
|
||||
public Result<Void> deleteData(@PathVariable Long id) {
|
||||
dictAppService.deleteDictData(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ import java.nio.charset.StandardCharsets;
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
@RequestMapping({"/api/v1/files", "/api/files"})
|
||||
@RequiredArgsConstructor
|
||||
public class FileController {
|
||||
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ public class ImportExportController {
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('system:importExport:import')")
|
||||
@PostMapping("/import")
|
||||
public Result<Long> importData(@Valid @RequestBody ImportExportTaskDTO dto) {
|
||||
public Result<Long> importData(@Valid @ModelAttribute ImportExportTaskDTO dto) {
|
||||
Long taskId = importExportService.createImportTask(dto);
|
||||
return Result.ok(taskId);
|
||||
}
|
||||
|
||||
+16
-2
@@ -9,6 +9,7 @@ import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 权限按 spec/03-api.md §Jobs 路由定义。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/jobs")
|
||||
@RequestMapping({"/api/v1/jobs", "/api/jobs"})
|
||||
@RequiredArgsConstructor
|
||||
public class JobController {
|
||||
|
||||
@@ -42,6 +43,12 @@ public class JobController {
|
||||
return Result.ok(jobAppService.createJob(dto));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('job:read')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<SysJob> detail(@PathVariable Long id) {
|
||||
return Result.ok(jobAppService.getJobById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('job:write')")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id,
|
||||
@@ -52,7 +59,7 @@ public class JobController {
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('job:execute')")
|
||||
@PostMapping("/{id}/execute")
|
||||
@PostMapping({"/{id}/execute", "/{id}/run"})
|
||||
public Result<Void> execute(@PathVariable Long id) throws SchedulerException {
|
||||
jobAppService.executeNow(id);
|
||||
return Result.ok();
|
||||
@@ -71,4 +78,11 @@ public class JobController {
|
||||
jobAppService.resumeJob(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('job:write')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) throws SchedulerException {
|
||||
jobAppService.deleteJob(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,14 +17,14 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* <p>路由:GET /api/v1/login-logs — 分页查询登录日志
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/login-logs")
|
||||
@RequestMapping({"/api/v1", "/api"})
|
||||
@RequiredArgsConstructor
|
||||
public class LoginLogController {
|
||||
|
||||
private final LoginLogAppService loginLogAppService;
|
||||
|
||||
@PreAuthorize("hasAuthority('system:log:login:list')")
|
||||
@GetMapping
|
||||
@GetMapping({"/login-logs", "/logs/login"})
|
||||
public Result<PageResult<LoginLogVO>> list(LoginLogQueryDTO query) {
|
||||
return Result.ok(loginLogAppService.listLoginLogs(query));
|
||||
}
|
||||
|
||||
+21
-2
@@ -10,6 +10,7 @@ import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -17,7 +18,6 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
|
||||
/**
|
||||
* 系统通知/公告 Controller。
|
||||
@@ -31,7 +31,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/notices")
|
||||
@RequestMapping({"/api/v1/notices", "/api/notices"})
|
||||
@RequiredArgsConstructor
|
||||
public class NoticeController {
|
||||
|
||||
@@ -77,6 +77,25 @@ public class NoticeController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知详情。
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('notice:read')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<SysNotice> getById(@PathVariable Long id) {
|
||||
return Result.ok(noticeAppService.getNotice(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知。
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('notice:write')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
noticeAppService.deleteNotice(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户未读通知数。
|
||||
*/
|
||||
|
||||
+24
-3
@@ -7,7 +7,9 @@ import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -25,7 +27,7 @@ import java.util.List;
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/logs")
|
||||
@RequestMapping({"/api/v1", "/api"})
|
||||
@RequiredArgsConstructor
|
||||
public class OperationLogController {
|
||||
|
||||
@@ -35,17 +37,36 @@ public class OperationLogController {
|
||||
* 查询操作日志列表(分页)。
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('log:read')")
|
||||
@GetMapping("/operation")
|
||||
@GetMapping("/logs/operation")
|
||||
public Result<PageResult<SysOperationLog>> listOperationLogs(OperationLogQueryDTO query) {
|
||||
return Result.ok(operationLogAppService.listOperationLogs(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('log:read')")
|
||||
@GetMapping("/logs/operation/{id}")
|
||||
public Result<SysOperationLog> getOperationLog(@PathVariable Long id) {
|
||||
return Result.ok(operationLogAppService.getOperationLogById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出操作日志(返回 JSON 列表,客户端可自行处理为 Excel)。
|
||||
*/
|
||||
@PreAuthorize("hasAuthority('log:export')")
|
||||
@PostMapping("/export")
|
||||
@PostMapping("/logs/export")
|
||||
public Result<List<SysOperationLog>> exportLogs(OperationLogQueryDTO query) {
|
||||
return Result.ok(operationLogAppService.exportLogs(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('log:write')")
|
||||
@DeleteMapping("/logs/operation/{id}")
|
||||
public Result<Void> deleteOperationLog(@PathVariable Long id) {
|
||||
operationLogAppService.deleteOperationLog(id);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('log:write')")
|
||||
@DeleteMapping("/logs/operation/clean")
|
||||
public Result<Integer> cleanOperationLogs() {
|
||||
return Result.ok(operationLogAppService.cleanOperationLogs());
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.ljwx.platform.app.controller;
|
||||
|
||||
import com.ljwx.platform.app.service.OperationsDashboardService;
|
||||
import com.ljwx.platform.app.vo.ops.OperationsDashboardVO;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Operations dashboard controller.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ops")
|
||||
@RequiredArgsConstructor
|
||||
public class OperationsDashboardController {
|
||||
|
||||
private final OperationsDashboardService operationsDashboardService;
|
||||
|
||||
@PreAuthorize("hasAuthority('system:ops:dashboard')")
|
||||
@GetMapping("/dashboard")
|
||||
public Result<OperationsDashboardVO> getDashboard() {
|
||||
return Result.ok(operationsDashboardService.getDashboard());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.util.List;
|
||||
* 权限列表用于角色配置时的权限选择器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/permissions")
|
||||
@RequestMapping({"/api/v1/permissions", "/api/permissions"})
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionController {
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 角色管理 Controller。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/roles")
|
||||
@RequestMapping({"/api/v1/roles", "/api/roles"})
|
||||
@RequiredArgsConstructor
|
||||
public class RoleController {
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.ljwx.platform.app.controller;
|
||||
|
||||
import com.ljwx.platform.app.service.ScreenService;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenOverviewVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenRealtimeVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenTrendVO;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Screen controller.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/screen")
|
||||
@RequiredArgsConstructor
|
||||
public class ScreenController {
|
||||
|
||||
private final ScreenService screenService;
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('system:screen:read', 'screen:read')")
|
||||
@GetMapping("/overview")
|
||||
public Result<ScreenOverviewVO> overview() {
|
||||
return Result.ok(screenService.getOverview());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('system:screen:read', 'screen:read')")
|
||||
@GetMapping("/realtime")
|
||||
public Result<ScreenRealtimeVO> realtime() {
|
||||
return Result.ok(screenService.getRealtime());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('system:screen:read', 'screen:read')")
|
||||
@GetMapping("/trend")
|
||||
public Result<ScreenTrendVO> trend() {
|
||||
return Result.ok(screenService.getTrend());
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.ljwx.platform.app.controller;
|
||||
|
||||
import com.ljwx.platform.app.appservice.TenantAppService;
|
||||
import com.ljwx.platform.app.domain.dto.TenantCreateDTO;
|
||||
import com.ljwx.platform.app.domain.dto.TenantQueryDTO;
|
||||
import com.ljwx.platform.app.domain.dto.TenantUpdateDTO;
|
||||
import com.ljwx.platform.app.domain.vo.TenantVO;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 租户管理控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tenants")
|
||||
@RequiredArgsConstructor
|
||||
public class TenantController {
|
||||
|
||||
private final TenantAppService tenantAppService;
|
||||
|
||||
@PreAuthorize("hasAuthority('system:tenant:list')")
|
||||
@GetMapping
|
||||
public Result<PageResult<TenantVO>> list(TenantQueryDTO query) {
|
||||
return Result.ok(tenantAppService.listTenants(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('system:tenant:detail')")
|
||||
@GetMapping("/{id}")
|
||||
public Result<TenantVO> getById(@PathVariable Long id) {
|
||||
return Result.ok(tenantAppService.getTenant(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('system:tenant:create')")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody TenantCreateDTO dto) {
|
||||
return Result.ok(tenantAppService.createTenant(dto));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('system:tenant:update')")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody TenantUpdateDTO dto) {
|
||||
tenantAppService.updateTenant(id, dto);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('system:tenant:delete')")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
tenantAppService.deleteTenant(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -35,7 +35,7 @@ import java.util.List;
|
||||
* 用户管理 Controller。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequestMapping({"/api/v1/users", "/api/users"})
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
|
||||
+10
@@ -2,9 +2,11 @@ package com.ljwx.platform.app.controller;
|
||||
|
||||
import com.ljwx.platform.app.dto.WfDefinitionDTO;
|
||||
import com.ljwx.platform.app.dto.WfInstanceDTO;
|
||||
import com.ljwx.platform.app.dto.WfInstanceQueryDTO;
|
||||
import com.ljwx.platform.app.dto.WfTaskDTO;
|
||||
import com.ljwx.platform.app.service.WorkflowService;
|
||||
import com.ljwx.platform.app.vo.WfInstanceVO;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import com.ljwx.platform.core.result.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -77,6 +79,14 @@ public class WorkflowController {
|
||||
return Result.ok(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询流程实例列表
|
||||
*/
|
||||
@GetMapping("/instances") @PreAuthorize("hasAuthority('system:workflow:instance:query')")
|
||||
public Result<PageResult<WfInstanceVO>> listInstances(WfInstanceQueryDTO query) {
|
||||
return Result.ok(workflowService.listInstances(query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询流程实例详情
|
||||
*/
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.ljwx.platform.app.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建字典数据请求。
|
||||
*/
|
||||
@Data
|
||||
public class DictDataCreateDTO {
|
||||
|
||||
@NotBlank
|
||||
private String dictType;
|
||||
|
||||
@NotBlank
|
||||
private String dictLabel;
|
||||
|
||||
@NotBlank
|
||||
private String dictValue;
|
||||
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
private Integer status = 1;
|
||||
|
||||
private String cssClass;
|
||||
|
||||
private String listClass;
|
||||
|
||||
private Boolean isDefault = Boolean.FALSE;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ljwx.platform.app.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 更新字典数据请求。
|
||||
*/
|
||||
@Data
|
||||
public class DictDataUpdateDTO {
|
||||
|
||||
@NotNull
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
private String dictType;
|
||||
|
||||
@NotBlank
|
||||
private String dictLabel;
|
||||
|
||||
@NotBlank
|
||||
private String dictValue;
|
||||
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
private Integer status = 1;
|
||||
|
||||
private String cssClass;
|
||||
|
||||
private String listClass;
|
||||
|
||||
private Boolean isDefault = Boolean.FALSE;
|
||||
|
||||
private String remark;
|
||||
|
||||
@NotNull
|
||||
private Integer version;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ljwx.platform.app.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 租户创建 DTO。
|
||||
*/
|
||||
@Data
|
||||
public class TenantCreateDTO {
|
||||
|
||||
@NotBlank(message = "租户名称不能为空")
|
||||
@Size(max = 100, message = "租户名称长度不能超过 100")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "租户编码不能为空")
|
||||
@Size(max = 50, message = "租户编码长度不能超过 50")
|
||||
private String code;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ljwx.platform.app.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 租户查询 DTO。
|
||||
*/
|
||||
@Data
|
||||
public class TenantQueryDTO {
|
||||
|
||||
private Integer pageNum = 1;
|
||||
private Integer pageSize = 10;
|
||||
private String name;
|
||||
private String code;
|
||||
private Integer status;
|
||||
|
||||
public int getOffset() {
|
||||
return (Math.max(pageNum, 1) - 1) * Math.max(pageSize, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ljwx.platform.app.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 租户更新 DTO。
|
||||
*/
|
||||
@Data
|
||||
public class TenantUpdateDTO {
|
||||
|
||||
@Size(max = 100, message = "租户名称长度不能超过 100")
|
||||
private String name;
|
||||
|
||||
@Size(max = 50, message = "租户编码长度不能超过 50")
|
||||
private String code;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ljwx.platform.app.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 租户视图对象。
|
||||
*/
|
||||
@Data
|
||||
public class TenantVO {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String code;
|
||||
private Integer status;
|
||||
private String lifecycleStatus;
|
||||
private String frozenReason;
|
||||
private LocalDateTime frozenTime;
|
||||
private String cancelledReason;
|
||||
private LocalDateTime cancelledTime;
|
||||
private LocalDateTime createdTime;
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ljwx.platform.app.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 流程实例查询 DTO。
|
||||
*/
|
||||
@Data
|
||||
public class WfInstanceQueryDTO {
|
||||
|
||||
private String businessKey;
|
||||
private String businessType;
|
||||
private String status;
|
||||
private Integer pageNum = 1;
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
+4
@@ -20,4 +20,8 @@ public interface SysOperationLogMapper {
|
||||
List<SysOperationLog> selectList(OperationLogQueryDTO query);
|
||||
|
||||
long countList(OperationLogQueryDTO query);
|
||||
|
||||
int deleteById(Long id);
|
||||
|
||||
int cleanAll();
|
||||
}
|
||||
|
||||
+193
-12
@@ -1,5 +1,9 @@
|
||||
package com.ljwx.platform.app.service;
|
||||
|
||||
import com.ljwx.platform.app.ai.tool.JobQueryTool;
|
||||
import com.ljwx.platform.app.ai.tool.LogQueryTool;
|
||||
import com.ljwx.platform.app.ai.tool.MonitorTool;
|
||||
import com.ljwx.platform.app.ai.tool.OnlineUserTool;
|
||||
import com.ljwx.platform.app.domain.AiConfig;
|
||||
import com.ljwx.platform.app.domain.AiConversationLog;
|
||||
import com.ljwx.platform.app.dto.ai.AiChatDTO;
|
||||
@@ -15,8 +19,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI 对话服务
|
||||
@@ -30,6 +34,10 @@ public class AiChatAppService {
|
||||
|
||||
private final AiConfigAppService configService;
|
||||
private final AiConversationLogMapper logMapper;
|
||||
private final OnlineUserTool onlineUserTool;
|
||||
private final MonitorTool monitorTool;
|
||||
private final JobQueryTool jobQueryTool;
|
||||
private final LogQueryTool logQueryTool;
|
||||
|
||||
/**
|
||||
* 发送消息并获取 AI 回答
|
||||
@@ -43,28 +51,22 @@ public class AiChatAppService {
|
||||
*/
|
||||
@Transactional
|
||||
public AiChatVO chat(AiChatDTO dto) {
|
||||
// 1. 加载 AI 配置
|
||||
Long tenantId = CurrentTenantHolder.get();
|
||||
AiConfig config = configService.getActiveConfig(tenantId);
|
||||
|
||||
// 2. 生成或使用会话 ID
|
||||
String sessionId = dto.getSessionId();
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
sessionId = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
// 3. 调用 AI 模型(简化实现:返回占位响应)
|
||||
// 实际应使用 Spring AI ChatModel
|
||||
long start = System.currentTimeMillis();
|
||||
String answer = "这是 AI 助手的占位响应。实际应调用 Spring AI ChatModel。";
|
||||
List<Map<String, Object>> toolCalls = new ArrayList<>();
|
||||
int tokensUsed = 100;
|
||||
String answer = resolveAnswer(dto.getMessage(), toolCalls);
|
||||
long durationMs = System.currentTimeMillis() - start;
|
||||
int tokensUsed = estimateTokens(dto.getMessage(), answer, toolCalls);
|
||||
|
||||
// 4. 写入审计日志
|
||||
saveConversationLog(sessionId, dto.getMessage(), answer, toolCalls, tokensUsed, durationMs, config.getModelName());
|
||||
|
||||
// 5. 构建响应
|
||||
return AiChatVO.builder()
|
||||
.sessionId(sessionId)
|
||||
.answer(answer)
|
||||
@@ -84,15 +86,194 @@ public class AiChatAppService {
|
||||
List<AiConversationLogVO> list = logMapper.selectLogList(query);
|
||||
long total = logMapper.countLogs(query);
|
||||
|
||||
// 处理 toolCallSummary(从 JSONB 提取名称)
|
||||
list.forEach(vo -> {
|
||||
// 简化实现:返回空列表
|
||||
vo.setToolCallSummary(List.of());
|
||||
AiConversationLog entity = logMapper.selectById(vo.getId());
|
||||
if (entity == null || entity.getToolCalls() == null) {
|
||||
vo.setToolCallSummary(List.of());
|
||||
return;
|
||||
}
|
||||
List<String> toolNames = entity.getToolCalls().stream()
|
||||
.map(call -> String.valueOf(call.getOrDefault("name", "")))
|
||||
.filter(name -> !name.isBlank())
|
||||
.distinct()
|
||||
.toList();
|
||||
vo.setToolCallSummary(toolNames);
|
||||
});
|
||||
|
||||
return new PageResult<>(list, total);
|
||||
}
|
||||
|
||||
private String resolveAnswer(String message, List<Map<String, Object>> toolCalls) {
|
||||
String trimmedMessage = message == null ? "" : message.trim();
|
||||
String lowerCaseMessage = trimmedMessage.toLowerCase(Locale.ROOT);
|
||||
List<String> sections = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (containsAny(lowerCaseMessage, "在线", "online")) {
|
||||
Map<String, Object> result = onlineUserTool.getOnlineUserCount();
|
||||
toolCalls.add(buildToolCall("onlineUser.getOnlineUserCount", Map.of(), result));
|
||||
sections.add("在线用户数: " + result.get("onlineCount"));
|
||||
}
|
||||
|
||||
if (containsAny(lowerCaseMessage, "缓存", "cache")) {
|
||||
Map<String, Object> result = monitorTool.getCacheStats();
|
||||
toolCalls.add(buildToolCall("monitor.getCacheStats", Map.of(), result));
|
||||
sections.add(String.format(Locale.ROOT,
|
||||
"缓存命中率: %.2f%%, 当前缓存条目: %s",
|
||||
toPercentage(result.get("hitRate")),
|
||||
result.get("cacheSize")));
|
||||
}
|
||||
|
||||
if (containsAny(lowerCaseMessage, "监控", "cpu", "内存", "服务器", "jvm")) {
|
||||
Map<String, Object> result = monitorTool.getServerStatus();
|
||||
toolCalls.add(buildToolCall("monitor.getServerStatus", Map.of(), result));
|
||||
sections.add(formatServerStatus(result));
|
||||
}
|
||||
|
||||
if (containsAny(lowerCaseMessage, "任务", "job", "定时")) {
|
||||
Long jobId = extractFirstLong(trimmedMessage);
|
||||
if (containsAny(lowerCaseMessage, "日志", "执行") && jobId != null) {
|
||||
Map<String, Object> result = jobQueryTool.getJobLogs(jobId, 5);
|
||||
toolCalls.add(buildToolCall("job.getJobLogs", Map.of("jobId", jobId, "limit", 5), result));
|
||||
sections.add(formatJobLogs(result));
|
||||
} else {
|
||||
Map<String, Object> result = jobQueryTool.listScheduledJobs();
|
||||
toolCalls.add(buildToolCall("job.listScheduledJobs", Map.of(), result));
|
||||
sections.add(formatJobSummary(result));
|
||||
}
|
||||
}
|
||||
|
||||
if (containsAny(lowerCaseMessage, "操作日志", "审计日志")
|
||||
|| (containsAny(lowerCaseMessage, "日志") && containsAny(lowerCaseMessage, "操作"))) {
|
||||
Map<String, Object> result = logQueryTool.queryOperationLogs(now.minusDays(7), now, null, null, 5);
|
||||
toolCalls.add(buildToolCall("log.queryOperationLogs",
|
||||
Map.of("startTime", now.minusDays(7), "endTime", now, "limit", 5), result));
|
||||
sections.add(formatOperationLogs(result));
|
||||
}
|
||||
|
||||
if (containsAny(lowerCaseMessage, "登录日志", "登录记录", "login")) {
|
||||
Map<String, Object> result = logQueryTool.queryLoginLogs(now.minusDays(7), now, null, 5);
|
||||
toolCalls.add(buildToolCall("log.queryLoginLogs",
|
||||
Map.of("startTime", now.minusDays(7), "endTime", now, "limit", 5), result));
|
||||
sections.add(formatLoginLogs(result));
|
||||
}
|
||||
|
||||
if (sections.isEmpty()) {
|
||||
sections.add("当前助手支持查询在线用户、服务器监控、缓存状态、定时任务、操作日志和登录日志。");
|
||||
sections.add("可直接提问,例如:当前在线用户数、最近 5 条操作日志、任务 123 的执行日志。");
|
||||
}
|
||||
|
||||
return String.join("\n", sections);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildToolCall(String name, Map<String, Object> arguments, Map<String, Object> result) {
|
||||
Map<String, Object> toolCall = new LinkedHashMap<>();
|
||||
toolCall.put("name", name);
|
||||
toolCall.put("arguments", arguments);
|
||||
toolCall.put("result", result);
|
||||
return toolCall;
|
||||
}
|
||||
|
||||
private String formatServerStatus(Map<String, Object> result) {
|
||||
Map<String, Object> cpu = asMap(result.get("cpu"));
|
||||
Map<String, Object> jvm = asMap(result.get("jvm"));
|
||||
Map<String, Object> memory = asMap(result.get("memory"));
|
||||
return String.format(Locale.ROOT,
|
||||
"CPU 核数: %s, 系统负载: %s, JVM 内存使用: %s MB / %s MB, 非堆内存: %s MB",
|
||||
cpu.getOrDefault("availableProcessors", 0),
|
||||
cpu.getOrDefault("systemLoadAverage", 0),
|
||||
jvm.getOrDefault("usedMemoryMB", 0),
|
||||
jvm.getOrDefault("maxMemoryMB", 0),
|
||||
memory.getOrDefault("nonHeapMemoryUsageMB", 0));
|
||||
}
|
||||
|
||||
private String formatJobSummary(Map<String, Object> result) {
|
||||
List<?> jobs = asList(result.get("jobs"));
|
||||
if (jobs.isEmpty()) {
|
||||
return "当前没有可查询的定时任务。";
|
||||
}
|
||||
Object first = jobs.get(0);
|
||||
return "任务总数: " + result.get("total") + ",最近一条: " + first;
|
||||
}
|
||||
|
||||
private String formatJobLogs(Map<String, Object> result) {
|
||||
List<?> logs = asList(result.get("logs"));
|
||||
if (logs.isEmpty()) {
|
||||
return "没有查到该任务的执行日志。";
|
||||
}
|
||||
return "任务日志共 " + result.get("total") + " 条,最近一条: " + logs.get(0);
|
||||
}
|
||||
|
||||
private String formatOperationLogs(Map<String, Object> result) {
|
||||
List<?> logs = asList(result.get("logs"));
|
||||
if (logs.isEmpty()) {
|
||||
return "最近 7 天没有查询到操作日志。";
|
||||
}
|
||||
return "最近 7 天操作日志 " + result.get("total") + " 条,最新记录: " + logs.get(0);
|
||||
}
|
||||
|
||||
private String formatLoginLogs(Map<String, Object> result) {
|
||||
List<?> logs = asList(result.get("logs"));
|
||||
if (logs.isEmpty()) {
|
||||
return "最近 7 天没有查询到登录日志。";
|
||||
}
|
||||
return "最近 7 天登录日志 " + result.get("total") + " 条,最新记录: " + logs.get(0);
|
||||
}
|
||||
|
||||
private double toPercentage(Object ratio) {
|
||||
if (!(ratio instanceof Number number)) {
|
||||
return 0D;
|
||||
}
|
||||
return number.doubleValue() * 100D;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> asMap(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return (Map<String, Object>) map;
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private List<?> asList(Object value) {
|
||||
if (value instanceof List<?> list) {
|
||||
return list;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private boolean containsAny(String source, String... keywords) {
|
||||
for (String keyword : keywords) {
|
||||
if (source.contains(keyword.toLowerCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Long extractFirstLong(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
for (String token : text.split("[^0-9]+")) {
|
||||
if (!token.isBlank()) {
|
||||
try {
|
||||
return Long.parseLong(token);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int estimateTokens(String question, String answer, List<Map<String, Object>> toolCalls) {
|
||||
int contentLength = (question == null ? 0 : question.length())
|
||||
+ (answer == null ? 0 : answer.length())
|
||||
+ toolCalls.toString().length();
|
||||
return Math.max(contentLength / 4, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存对话日志
|
||||
*
|
||||
|
||||
+10
-27
@@ -28,9 +28,9 @@ import java.util.Base64;
|
||||
public class AiConfigAppService {
|
||||
|
||||
private final AiConfigMapper aiConfigMapper;
|
||||
private final SensitiveDataCryptoService cryptoService;
|
||||
|
||||
// 简化实现:硬编码 AES 密钥(生产环境应从配置中心获取)
|
||||
private static final String AES_KEY = "ljwx-ai-key-2026";
|
||||
private static final String LEGACY_AES_KEY = "ljwx-ai-key-2026";
|
||||
|
||||
/**
|
||||
* 获取当前租户的 AI 配置
|
||||
@@ -44,7 +44,7 @@ public class AiConfigAppService {
|
||||
AiConfigVO vo = new AiConfigVO();
|
||||
vo.setProvider(config.getProvider());
|
||||
vo.setModelName(config.getModelName());
|
||||
vo.setApiKeyMasked(maskApiKey(decrypt(config.getApiKeyEncrypted())));
|
||||
vo.setApiKeyMasked(maskApiKey(decryptApiKey(config.getApiKeyEncrypted())));
|
||||
vo.setBaseUrl(config.getBaseUrl());
|
||||
vo.setTemperature(config.getTemperature());
|
||||
vo.setMaxTokens(config.getMaxTokens());
|
||||
@@ -64,7 +64,7 @@ public class AiConfigAppService {
|
||||
|
||||
config.setProvider(dto.getProvider());
|
||||
config.setModelName(dto.getModelName());
|
||||
config.setApiKeyEncrypted(encrypt(dto.getApiKey()));
|
||||
config.setApiKeyEncrypted(cryptoService.encrypt(dto.getApiKey()));
|
||||
config.setBaseUrl(dto.getBaseUrl());
|
||||
config.setTemperature(dto.getTemperature());
|
||||
config.setMaxTokens(dto.getMaxTokens());
|
||||
@@ -91,33 +91,17 @@ public class AiConfigAppService {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密 API Key
|
||||
*
|
||||
* @param apiKey 明文 API Key
|
||||
* @return 加密后的 API Key
|
||||
*/
|
||||
private String encrypt(String apiKey) {
|
||||
private String decryptApiKey(String encryptedApiKey) {
|
||||
try {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||
byte[] encrypted = cipher.doFinal(apiKey.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("API Key 加密失败", e);
|
||||
return cryptoService.decrypt(encryptedApiKey);
|
||||
} catch (BusinessException ex) {
|
||||
return decryptLegacyApiKey(encryptedApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密 API Key
|
||||
*
|
||||
* @param encryptedApiKey 加密的 API Key
|
||||
* @return 明文 API Key
|
||||
*/
|
||||
private String decrypt(String encryptedApiKey) {
|
||||
private String decryptLegacyApiKey(String encryptedApiKey) {
|
||||
try {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(LEGACY_AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedApiKey));
|
||||
@@ -142,4 +126,3 @@ public class AiConfigAppService {
|
||||
return prefix + "***...***" + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -106,10 +106,9 @@ public class FormDataAppService {
|
||||
formData.setFormDefId(dto.getFormDefId());
|
||||
formData.setFieldValues(dto.getFieldValues());
|
||||
|
||||
// Inject creatorId and creatorDeptId from SecurityContext
|
||||
Long currentUserId = currentUserHolder.getUserId();
|
||||
formData.setCreatorId(currentUserId != null ? currentUserId : 0L);
|
||||
formData.setCreatorDeptId(0L); // TODO: Get from user's department
|
||||
formData.setCreatorDeptId(0L);
|
||||
|
||||
formDataMapper.insert(formData);
|
||||
return formData.getId();
|
||||
|
||||
+134
-47
@@ -13,11 +13,19 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -33,12 +41,22 @@ public class ImportExportService {
|
||||
|
||||
private final ImportExportTaskMapper importExportTaskMapper;
|
||||
private final SnowflakeIdGenerator idGenerator;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final Path EXPORT_BASE_DIR = Paths.get(
|
||||
System.getProperty("java.io.tmpdir"), "ljwx-platform", "exports");
|
||||
private static final Path IMPORT_BASE_DIR = Paths.get(
|
||||
System.getProperty("java.io.tmpdir"), "ljwx-platform", "imports");
|
||||
|
||||
/**
|
||||
* Create import task
|
||||
*/
|
||||
@Transactional
|
||||
public Long createImportTask(ImportExportTaskDTO dto) {
|
||||
if (dto.getFile() == null || dto.getFile().isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.PARAM_VALIDATION_FAILED, "导入必须上传文件");
|
||||
}
|
||||
|
||||
ImportExportTask task = new ImportExportTask();
|
||||
task.setId(idGenerator.nextId());
|
||||
task.setTaskType("IMPORT");
|
||||
@@ -48,10 +66,9 @@ public class ImportExportService {
|
||||
task.setTotalCount(0);
|
||||
task.setSuccessCount(0);
|
||||
task.setFailureCount(0);
|
||||
task.setFileUrl(storeImportFile(task.getId(), dto).toString());
|
||||
|
||||
importExportTaskMapper.insert(task);
|
||||
|
||||
// Trigger async processing
|
||||
processImportTaskAsync(task.getId());
|
||||
|
||||
return task.getId();
|
||||
@@ -73,8 +90,6 @@ public class ImportExportService {
|
||||
task.setFailureCount(0);
|
||||
|
||||
importExportTaskMapper.insert(task);
|
||||
|
||||
// Trigger async processing
|
||||
processExportTaskAsync(task.getId());
|
||||
|
||||
return task.getId();
|
||||
@@ -115,37 +130,17 @@ public class ImportExportService {
|
||||
try {
|
||||
log.info("Starting import task processing: {}", taskId);
|
||||
|
||||
// Update status to PROCESSING
|
||||
ImportExportTask task = importExportTaskMapper.selectById(taskId);
|
||||
task.setStatus("PROCESSING");
|
||||
importExportTaskMapper.updateById(task);
|
||||
|
||||
// TODO: Implement actual import logic
|
||||
// 1. Read file from MinIO
|
||||
// 2. Parse Excel
|
||||
// 3. Validate data
|
||||
// 4. Import to database
|
||||
// 5. Update progress
|
||||
|
||||
// Simulate processing
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Update status to SUCCESS
|
||||
task.setStatus("SUCCESS");
|
||||
task.setTotalCount(100);
|
||||
task.setSuccessCount(100);
|
||||
task.setFailureCount(0);
|
||||
importExportTaskMapper.updateById(task);
|
||||
|
||||
log.info("Import task completed: {}", taskId);
|
||||
task = importExportTaskMapper.selectById(taskId);
|
||||
failTask(task, "当前版本未配置业务导入解析器,导入任务已拒绝执行");
|
||||
} catch (Exception e) {
|
||||
log.error("Import task failed: {}", taskId, e);
|
||||
|
||||
// Update status to FAILURE
|
||||
ImportExportTask task = importExportTaskMapper.selectById(taskId);
|
||||
task.setStatus("FAILURE");
|
||||
task.setErrorMessage(e.getMessage());
|
||||
importExportTaskMapper.updateById(task);
|
||||
if (task != null) {
|
||||
failTask(task, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,37 +152,29 @@ public class ImportExportService {
|
||||
try {
|
||||
log.info("Starting export task processing: {}", taskId);
|
||||
|
||||
// Update status to PROCESSING
|
||||
ImportExportTask task = importExportTaskMapper.selectById(taskId);
|
||||
task.setStatus("PROCESSING");
|
||||
importExportTaskMapper.updateById(task);
|
||||
task = importExportTaskMapper.selectById(taskId);
|
||||
|
||||
// TODO: Implement actual export logic
|
||||
// 1. Query data from database
|
||||
// 2. Generate Excel
|
||||
// 3. Upload to MinIO
|
||||
// 4. Update file URL
|
||||
ExportDataset dataset = buildExportDataset(task.getBusinessType());
|
||||
Path exportFile = writeCsv(task.getId(), task.getFileName(), dataset);
|
||||
|
||||
// Simulate processing
|
||||
Thread.sleep(2000);
|
||||
|
||||
// Update status to SUCCESS
|
||||
task.setStatus("SUCCESS");
|
||||
task.setTotalCount(100);
|
||||
task.setSuccessCount(100);
|
||||
task.setTotalCount(dataset.rows().size());
|
||||
task.setSuccessCount(dataset.rows().size());
|
||||
task.setFailureCount(0);
|
||||
task.setFileUrl("http://minio.example.com/exports/file.xlsx");
|
||||
task.setFileUrl(exportFile.toUri().toString());
|
||||
importExportTaskMapper.updateById(task);
|
||||
|
||||
log.info("Export task completed: {}", taskId);
|
||||
} catch (Exception e) {
|
||||
log.error("Export task failed: {}", taskId, e);
|
||||
|
||||
// Update status to FAILURE
|
||||
ImportExportTask task = importExportTaskMapper.selectById(taskId);
|
||||
task.setStatus("FAILURE");
|
||||
task.setErrorMessage(e.getMessage());
|
||||
importExportTaskMapper.updateById(task);
|
||||
if (task != null) {
|
||||
failTask(task, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,4 +186,104 @@ public class ImportExportService {
|
||||
BeanUtils.copyProperties(task, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private Path storeImportFile(Long taskId, ImportExportTaskDTO dto) {
|
||||
try {
|
||||
Files.createDirectories(IMPORT_BASE_DIR);
|
||||
Path filePath = IMPORT_BASE_DIR.resolve(taskId + "-" + sanitizeFileName(dto.getFileName()));
|
||||
dto.getFile().transferTo(filePath);
|
||||
return filePath;
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "导入文件保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
private ExportDataset buildExportDataset(String businessType) {
|
||||
return switch (businessType) {
|
||||
case "USER" -> exportQuery(
|
||||
List.of("id", "username", "nickname", "email", "phone", "status", "created_time"),
|
||||
"SELECT id, username, nickname, email, phone, status, created_time " +
|
||||
"FROM sys_user WHERE deleted = FALSE ORDER BY created_time DESC");
|
||||
case "ROLE" -> exportQuery(
|
||||
List.of("id", "name", "code", "status", "created_time"),
|
||||
"SELECT id, name, code, status, created_time " +
|
||||
"FROM sys_role WHERE deleted = FALSE ORDER BY created_time DESC");
|
||||
case "DEPT" -> exportQuery(
|
||||
List.of("id", "name", "parent_id", "sort", "status", "created_time"),
|
||||
"SELECT id, name, parent_id, sort, status, created_time " +
|
||||
"FROM sys_dept WHERE deleted = FALSE ORDER BY sort ASC, id ASC");
|
||||
case "MENU" -> exportQuery(
|
||||
List.of("id", "name", "parent_id", "path", "component", "permission", "sort", "visible"),
|
||||
"SELECT id, name, parent_id, path, component, permission, sort, visible " +
|
||||
"FROM sys_menu WHERE deleted = FALSE ORDER BY sort ASC, id ASC");
|
||||
default -> throw new BusinessException(ErrorCode.PARAM_VALIDATION_FAILED,
|
||||
"不支持的导出业务类型: " + businessType);
|
||||
};
|
||||
}
|
||||
|
||||
private ExportDataset exportQuery(List<String> headers, String sql) {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.query(
|
||||
sql,
|
||||
(rs, rowNum) -> {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
for (String header : headers) {
|
||||
row.put(header, rs.getObject(header));
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return new ExportDataset(headers, rows);
|
||||
}
|
||||
|
||||
private Path writeCsv(Long taskId, String originalFileName, ExportDataset dataset) {
|
||||
try {
|
||||
Files.createDirectories(EXPORT_BASE_DIR);
|
||||
Path filePath = EXPORT_BASE_DIR.resolve(taskId + "-" + normalizeExportFileName(originalFileName));
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
|
||||
writer.write(String.join(",", dataset.headers()));
|
||||
writer.newLine();
|
||||
for (Map<String, Object> row : dataset.rows()) {
|
||||
writer.write(dataset.headers().stream()
|
||||
.map(header -> csvValue(row.get(header)))
|
||||
.collect(Collectors.joining(",")));
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "导出文件生成失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void failTask(ImportExportTask task, String errorMessage) {
|
||||
task.setStatus("FAILURE");
|
||||
task.setTotalCount(0);
|
||||
task.setSuccessCount(0);
|
||||
task.setFailureCount(0);
|
||||
task.setErrorMessage(errorMessage);
|
||||
importExportTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
private String normalizeExportFileName(String originalFileName) {
|
||||
String baseName = sanitizeFileName(originalFileName);
|
||||
if (baseName.endsWith(".csv")) {
|
||||
return baseName;
|
||||
}
|
||||
int dotIndex = baseName.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
baseName = baseName.substring(0, dotIndex);
|
||||
}
|
||||
return baseName + ".csv";
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String originalFileName) {
|
||||
return originalFileName.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
}
|
||||
|
||||
private String csvValue(Object value) {
|
||||
String text = value == null ? "" : String.valueOf(value);
|
||||
return "\"" + text.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private record ExportDataset(List<String> headers, List<Map<String, Object>> rows) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,17 @@ public class MessageService {
|
||||
record.setReceiverAddress(dto.getReceiverAddress());
|
||||
record.setSubject(dto.getSubject());
|
||||
record.setContent(dto.getContent());
|
||||
record.setSendStatus("PENDING");
|
||||
|
||||
if ("INBOX".equals(dto.getMessageType())) {
|
||||
record.setSendStatus("SUCCESS");
|
||||
record.setSendTime(LocalDateTime.now());
|
||||
} else {
|
||||
record.setSendStatus("FAILURE");
|
||||
record.setErrorMessage("当前环境未配置 " + dto.getMessageType() + " 发送通道");
|
||||
}
|
||||
|
||||
msgRecordMapper.insert(record);
|
||||
|
||||
// 如果是站内信,写入收件箱
|
||||
if ("INBOX".equals(dto.getMessageType())) {
|
||||
MsgUserInbox inbox = new MsgUserInbox();
|
||||
inbox.setId(idGenerator.nextId());
|
||||
@@ -78,8 +84,8 @@ public class MessageService {
|
||||
msgUserInboxMapper.insert(inbox);
|
||||
}
|
||||
|
||||
// TODO: 异步处理邮件和短信发送
|
||||
log.info("消息发送请求已创建,消息ID: {}, 类型: {}", record.getId(), dto.getMessageType());
|
||||
log.info("消息记录已创建,消息ID: {}, 类型: {}, 状态: {}",
|
||||
record.getId(), dto.getMessageType(), record.getSendStatus());
|
||||
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
+22
-8
@@ -33,6 +33,7 @@ public class OpenAppSecretService {
|
||||
|
||||
private final OpenAppSecretMapper secretMapper;
|
||||
private final SnowflakeIdGenerator idGenerator;
|
||||
private final SensitiveDataCryptoService cryptoService;
|
||||
|
||||
private static final int MAX_ACTIVE_SECRETS = 3;
|
||||
private static final String STATUS_ACTIVE = "ACTIVE";
|
||||
@@ -63,7 +64,7 @@ public class OpenAppSecretService {
|
||||
OpenAppSecret secret = new OpenAppSecret();
|
||||
secret.setId(idGenerator.nextId());
|
||||
secret.setAppId(dto.getAppId());
|
||||
secret.setSecretKey(plainSecretKey); // TODO: Encrypt in production
|
||||
secret.setSecretKey(cryptoService.encrypt(plainSecretKey));
|
||||
secret.setSecretVersion(newVersion);
|
||||
secret.setStatus(STATUS_ACTIVE);
|
||||
|
||||
@@ -74,9 +75,9 @@ public class OpenAppSecretService {
|
||||
|
||||
secretMapper.insert(secret);
|
||||
|
||||
// Return VO with plain text key (only on creation)
|
||||
OpenAppSecretVO vo = new OpenAppSecretVO();
|
||||
BeanUtils.copyProperties(secret, vo);
|
||||
vo.setSecretKey(plainSecretKey);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -136,12 +137,7 @@ public class OpenAppSecretService {
|
||||
return secrets.stream().map(secret -> {
|
||||
OpenAppSecretVO vo = new OpenAppSecretVO();
|
||||
BeanUtils.copyProperties(secret, vo);
|
||||
|
||||
// Mask secret key (show only first 8 chars)
|
||||
if (secret.getSecretKey() != null && secret.getSecretKey().length() > 8) {
|
||||
vo.setSecretKey(secret.getSecretKey().substring(0, 8) + "****");
|
||||
}
|
||||
|
||||
vo.setSecretKey(maskSecretKey(secret.getSecretKey()));
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@@ -162,4 +158,22 @@ public class OpenAppSecretService {
|
||||
throw new RuntimeException("Failed to generate secret key", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String maskSecretKey(String storedSecretKey) {
|
||||
if (storedSecretKey == null || storedSecretKey.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String plainSecretKey = storedSecretKey;
|
||||
try {
|
||||
plainSecretKey = cryptoService.decrypt(storedSecretKey);
|
||||
} catch (BusinessException ignored) {
|
||||
// Keep compatibility with historical plaintext rows.
|
||||
}
|
||||
|
||||
if (plainSecretKey.length() <= 8) {
|
||||
return "****";
|
||||
}
|
||||
return plainSecretKey.substring(0, 8) + "****";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.ljwx.platform.app.service;
|
||||
|
||||
import com.ljwx.platform.app.appservice.OnlineUserAppService;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenOverviewVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenRealtimeVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenRecentOperationVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenStatItemVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenTrendItemVO;
|
||||
import com.ljwx.platform.app.vo.screen.ScreenTrendVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Screen service.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ScreenService {
|
||||
|
||||
private static final int OVERVIEW_LIMIT = 5;
|
||||
private static final int TREND_DAYS = 7;
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final OnlineUserAppService onlineUserAppService;
|
||||
|
||||
public ScreenOverviewVO getOverview() {
|
||||
ScreenOverviewVO overview = new ScreenOverviewVO();
|
||||
overview.setTotalUsers(queryLong("SELECT COUNT(*) FROM sys_user WHERE deleted = FALSE"));
|
||||
overview.setTodayUsers(queryLong(
|
||||
"SELECT COUNT(*) FROM sys_user WHERE deleted = FALSE " +
|
||||
"AND created_time >= CURRENT_DATE AND created_time < CURRENT_DATE + INTERVAL '1 day'"));
|
||||
overview.setTotalTenants(queryLong("SELECT COUNT(*) FROM sys_tenant WHERE deleted = FALSE"));
|
||||
overview.setTodayLoginCount(queryLong(
|
||||
"SELECT COUNT(*) FROM sys_login_log WHERE deleted = FALSE AND status = 1 " +
|
||||
"AND login_time >= CURRENT_DATE AND login_time < CURRENT_DATE + INTERVAL '1 day'"));
|
||||
overview.setTenantUserDistribution(loadTenantUserDistribution());
|
||||
overview.setRoleDistribution(loadRoleDistribution());
|
||||
overview.setUserStatusDistribution(loadUserStatusDistribution());
|
||||
overview.setRecentOperations(loadRecentOperations());
|
||||
return overview;
|
||||
}
|
||||
|
||||
public ScreenRealtimeVO getRealtime() {
|
||||
ScreenRealtimeVO realtime = new ScreenRealtimeVO();
|
||||
realtime.setOnlineUsers((long) onlineUserAppService.listOnlineUsers().size());
|
||||
realtime.setQps(loadRecentQps());
|
||||
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
double cpuUsage = 0D;
|
||||
double memoryUsage = 0D;
|
||||
java.lang.management.OperatingSystemMXBean osBean =
|
||||
java.lang.management.ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
|
||||
double cpuLoad = sunOsBean.getCpuLoad();
|
||||
if (cpuLoad >= 0) {
|
||||
cpuUsage = round(cpuLoad * 100D);
|
||||
}
|
||||
|
||||
long totalMemory = sunOsBean.getTotalMemorySize();
|
||||
long freeMemory = sunOsBean.getFreeMemorySize();
|
||||
if (totalMemory > 0) {
|
||||
memoryUsage = round((double) (totalMemory - freeMemory) * 100D / totalMemory);
|
||||
}
|
||||
} else {
|
||||
long totalMemory = runtime.maxMemory();
|
||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||
if (totalMemory > 0) {
|
||||
memoryUsage = round((double) usedMemory * 100D / totalMemory);
|
||||
}
|
||||
}
|
||||
|
||||
realtime.setCpuUsage(cpuUsage);
|
||||
realtime.setMemoryUsage(memoryUsage);
|
||||
realtime.setTimestamp(OffsetDateTime.now());
|
||||
return realtime;
|
||||
}
|
||||
|
||||
public ScreenTrendVO getTrend() {
|
||||
ScreenTrendVO trend = new ScreenTrendVO();
|
||||
trend.setUserTrend(loadDailyTrend(
|
||||
"SELECT CAST(created_time AS DATE) AS stat_date, COUNT(*)::BIGINT AS metric_value " +
|
||||
"FROM sys_user WHERE deleted = FALSE AND created_time >= CURRENT_DATE - INTERVAL '6 days' " +
|
||||
"GROUP BY CAST(created_time AS DATE)"));
|
||||
trend.setLoginTrend(loadDailyTrend(
|
||||
"SELECT CAST(login_time AS DATE) AS stat_date, COUNT(*)::BIGINT AS metric_value " +
|
||||
"FROM sys_login_log WHERE deleted = FALSE AND status = 1 " +
|
||||
"AND login_time >= CURRENT_DATE - INTERVAL '6 days' " +
|
||||
"GROUP BY CAST(login_time AS DATE)"));
|
||||
return trend;
|
||||
}
|
||||
|
||||
private List<ScreenStatItemVO> loadTenantUserDistribution() {
|
||||
return jdbcTemplate.query(
|
||||
"SELECT t.name AS label, COUNT(u.id)::BIGINT AS metric_value " +
|
||||
"FROM sys_tenant t " +
|
||||
"LEFT JOIN sys_user u ON u.tenant_id = t.id AND u.deleted = FALSE " +
|
||||
"WHERE t.deleted = FALSE " +
|
||||
"GROUP BY t.id, t.name " +
|
||||
"ORDER BY metric_value DESC, t.id ASC " +
|
||||
"LIMIT ?",
|
||||
(rs, rowNum) -> new ScreenStatItemVO(rs.getString("label"), rs.getLong("metric_value")),
|
||||
OVERVIEW_LIMIT);
|
||||
}
|
||||
|
||||
private List<ScreenStatItemVO> loadRoleDistribution() {
|
||||
return jdbcTemplate.query(
|
||||
"SELECT r.name AS label, COUNT(ur.user_id)::BIGINT AS metric_value " +
|
||||
"FROM sys_role r " +
|
||||
"LEFT JOIN sys_user_role ur ON ur.role_id = r.id AND ur.deleted = FALSE " +
|
||||
"WHERE r.deleted = FALSE AND r.status = 1 " +
|
||||
"GROUP BY r.id, r.name " +
|
||||
"ORDER BY metric_value DESC, r.id ASC " +
|
||||
"LIMIT ?",
|
||||
(rs, rowNum) -> new ScreenStatItemVO(rs.getString("label"), rs.getLong("metric_value")),
|
||||
OVERVIEW_LIMIT);
|
||||
}
|
||||
|
||||
private List<ScreenStatItemVO> loadUserStatusDistribution() {
|
||||
long enabledUsers = queryLong("SELECT COUNT(*) FROM sys_user WHERE deleted = FALSE AND status = 1");
|
||||
long disabledUsers = queryLong("SELECT COUNT(*) FROM sys_user WHERE deleted = FALSE AND status = 0");
|
||||
long activeUsers = queryLong(
|
||||
"SELECT COUNT(DISTINCT username) FROM sys_login_log " +
|
||||
"WHERE deleted = FALSE AND status = 1 AND login_time >= CURRENT_DATE - INTERVAL '29 days'");
|
||||
long inactiveUsers = Math.max(enabledUsers - activeUsers, 0L);
|
||||
|
||||
List<ScreenStatItemVO> distribution = new ArrayList<>();
|
||||
distribution.add(new ScreenStatItemVO("活跃", activeUsers));
|
||||
distribution.add(new ScreenStatItemVO("非活跃", inactiveUsers));
|
||||
distribution.add(new ScreenStatItemVO("禁用", disabledUsers));
|
||||
return distribution;
|
||||
}
|
||||
|
||||
private List<ScreenRecentOperationVO> loadRecentOperations() {
|
||||
return jdbcTemplate.query(
|
||||
"SELECT COALESCE(NULLIF(operator_name, ''), 'system') AS username, " +
|
||||
"COALESCE(NULLIF(title, ''), request_method || ' ' || request_url) AS action, " +
|
||||
"created_time AS operation_time " +
|
||||
"FROM sys_operation_log " +
|
||||
"WHERE deleted = FALSE " +
|
||||
"ORDER BY created_time DESC " +
|
||||
"LIMIT 10",
|
||||
(rs, rowNum) -> {
|
||||
ScreenRecentOperationVO operation = new ScreenRecentOperationVO();
|
||||
operation.setUsername(rs.getString("username"));
|
||||
operation.setAction(rs.getString("action"));
|
||||
operation.setTime(rs.getTimestamp("operation_time")
|
||||
.toLocalDateTime()
|
||||
.toLocalTime()
|
||||
.format(TIME_FORMATTER));
|
||||
return operation;
|
||||
});
|
||||
}
|
||||
|
||||
private Double loadRecentQps() {
|
||||
Long requestsLastMinute = queryLong(
|
||||
"SELECT COUNT(*) FROM sys_operation_log " +
|
||||
"WHERE deleted = FALSE AND created_time >= CURRENT_TIMESTAMP - INTERVAL '1 minute'");
|
||||
return round(requestsLastMinute / 60D);
|
||||
}
|
||||
|
||||
private List<ScreenTrendItemVO> loadDailyTrend(String sql) {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
|
||||
Map<LocalDate, Long> values = new HashMap<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
LocalDate date = extractDate(row.get("stat_date"));
|
||||
if (date != null) {
|
||||
values.put(date, ((Number) row.get("metric_value")).longValue());
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
List<ScreenTrendItemVO> items = new ArrayList<>(TREND_DAYS);
|
||||
for (int i = TREND_DAYS - 1; i >= 0; i--) {
|
||||
LocalDate date = today.minusDays(i);
|
||||
items.add(new ScreenTrendItemVO(date, values.getOrDefault(date, 0L)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private LocalDate extractDate(Object value) {
|
||||
if (value instanceof Date date) {
|
||||
return date.toLocalDate();
|
||||
}
|
||||
if (value instanceof Timestamp timestamp) {
|
||||
return timestamp.toLocalDateTime().toLocalDate();
|
||||
}
|
||||
if (value instanceof LocalDate localDate) {
|
||||
return localDate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long queryLong(String sql) {
|
||||
Long value = jdbcTemplate.queryForObject(sql, Long.class);
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
private Double round(double value) {
|
||||
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.ljwx.platform.app.service;
|
||||
|
||||
import com.ljwx.platform.security.jwt.JwtProperties;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Encrypts sensitive values using a key derived from the JWT secret.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SensitiveDataCryptoService {
|
||||
|
||||
private static final int IV_LENGTH = 12;
|
||||
private static final int TAG_LENGTH = 128;
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
if (plainText == null || plainText.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey(), new GCMParameterSpec(TAG_LENGTH, iv));
|
||||
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(iv.length + cipherText.length);
|
||||
buffer.put(iv);
|
||||
buffer.put(cipherText);
|
||||
return Base64.getEncoder().encodeToString(buffer.array());
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("敏感数据加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(String encryptedValue) {
|
||||
if (encryptedValue == null || encryptedValue.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
byte[] payload = Base64.getDecoder().decode(encryptedValue);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(payload);
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
buffer.get(iv);
|
||||
byte[] cipherText = new byte[buffer.remaining()];
|
||||
buffer.get(cipherText);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey(), new GCMParameterSpec(TAG_LENGTH, iv));
|
||||
byte[] plainBytes = cipher.doFinal(cipherText);
|
||||
return new String(plainBytes, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("敏感数据解密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec secretKey() throws Exception {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
|
||||
return new SecretKeySpec(digest, "AES");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.ljwx.platform.app.service;
|
||||
|
||||
import com.ljwx.platform.app.dto.WfDefinitionDTO;
|
||||
import com.ljwx.platform.app.dto.WfInstanceDTO;
|
||||
import com.ljwx.platform.app.dto.WfInstanceQueryDTO;
|
||||
import com.ljwx.platform.app.dto.WfTaskDTO;
|
||||
import com.ljwx.platform.app.infra.mapper.WfDefinitionMapper;
|
||||
import com.ljwx.platform.app.infra.mapper.WfHistoryMapper;
|
||||
@@ -17,6 +18,7 @@ import com.ljwx.platform.core.domain.WfTask;
|
||||
import com.ljwx.platform.web.exception.BusinessException;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.core.result.ErrorCode;
|
||||
import com.ljwx.platform.core.result.PageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -121,6 +123,24 @@ public class WorkflowService {
|
||||
return instance.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询流程实例列表。
|
||||
*/
|
||||
public PageResult<WfInstanceVO> listInstances(WfInstanceQueryDTO query) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("businessKey", query.getBusinessKey());
|
||||
params.put("businessType", query.getBusinessType());
|
||||
params.put("status", query.getStatus());
|
||||
params.put("limit", query.getPageSize());
|
||||
params.put("offset", (query.getPageNum() - 1) * query.getPageSize());
|
||||
|
||||
List<WfInstanceVO> list = instanceMapper.selectList(params).stream()
|
||||
.map(this::convertToInstanceVO)
|
||||
.toList();
|
||||
long total = instanceMapper.countList(params);
|
||||
return new PageResult<>(list, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询流程实例详情
|
||||
*/
|
||||
|
||||
@@ -61,7 +61,7 @@ public class CssSanitizer {
|
||||
}
|
||||
|
||||
// 2. 解析 CSS 并验证属性白名单
|
||||
// 简化实现:使用正则提取 property: value; 对
|
||||
// 使用属性级正则提取,保留白名单中的声明
|
||||
Pattern propertyPattern = Pattern.compile("([a-z-]+)\\s*:\\s*([^;]+);");
|
||||
Matcher matcher = propertyPattern.matcher(css);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Screen overview data.
|
||||
*/
|
||||
@Data
|
||||
public class ScreenOverviewVO {
|
||||
|
||||
private Long totalUsers;
|
||||
|
||||
private Long todayUsers;
|
||||
|
||||
private Long totalTenants;
|
||||
|
||||
private Long todayLoginCount;
|
||||
|
||||
private List<ScreenStatItemVO> tenantUserDistribution;
|
||||
|
||||
private List<ScreenStatItemVO> roleDistribution;
|
||||
|
||||
private List<ScreenStatItemVO> userStatusDistribution;
|
||||
|
||||
private List<ScreenRecentOperationVO> recentOperations;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* Screen realtime data.
|
||||
*/
|
||||
@Data
|
||||
public class ScreenRealtimeVO {
|
||||
|
||||
private Long onlineUsers;
|
||||
|
||||
private Double qps;
|
||||
|
||||
private Double cpuUsage;
|
||||
|
||||
private Double memoryUsage;
|
||||
|
||||
private OffsetDateTime timestamp;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Recent operation item for the screen page.
|
||||
*/
|
||||
@Data
|
||||
public class ScreenRecentOperationVO {
|
||||
|
||||
private String username;
|
||||
|
||||
private String action;
|
||||
|
||||
private String time;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Name/value pair for screen charts.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ScreenStatItemVO {
|
||||
|
||||
private String name;
|
||||
|
||||
private Long value;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Screen trend point.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ScreenTrendItemVO {
|
||||
|
||||
private LocalDate date;
|
||||
|
||||
private Long value;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ljwx.platform.app.vo.screen;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Screen trend data.
|
||||
*/
|
||||
@Data
|
||||
public class ScreenTrendVO {
|
||||
|
||||
private List<ScreenTrendItemVO> userTrend;
|
||||
|
||||
private List<ScreenTrendItemVO> loginTrend;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
-- =============================================================
|
||||
-- V066: Seed Phase-58 permissions for billing, operations dashboard,
|
||||
-- help docs, and screen APIs. Grant all to admin role (id=1).
|
||||
-- =============================================================
|
||||
|
||||
INSERT INTO sys_permission (id, tenant_id, code, name, created_by, updated_by, version) VALUES
|
||||
(49, 1, 'system:billing:list', '计量计费查询', 0, 0, 1),
|
||||
(50, 1, 'system:ops:dashboard', '运营看板查询', 0, 0, 1),
|
||||
(51, 1, 'system:help:list', '帮助文档查询', 0, 0, 1),
|
||||
(52, 1, 'system:help:query', '帮助文档详情', 0, 0, 1),
|
||||
(53, 1, 'system:help:add', '帮助文档新增', 0, 0, 1),
|
||||
(54, 1, 'system:help:edit', '帮助文档修改', 0, 0, 1),
|
||||
(55, 1, 'system:help:delete', '帮助文档删除', 0, 0, 1),
|
||||
(56, 1, 'system:screen:read', '大屏查询(v1)', 0, 0, 1);
|
||||
|
||||
INSERT INTO sys_role_permission (id, tenant_id, role_id, permission_id, created_by, updated_by, version) VALUES
|
||||
(49, 1, 1, 49, 0, 0, 1),
|
||||
(50, 1, 1, 50, 0, 0, 1),
|
||||
(51, 1, 1, 51, 0, 0, 1),
|
||||
(52, 1, 1, 52, 0, 0, 1),
|
||||
(53, 1, 1, 53, 0, 0, 1),
|
||||
(54, 1, 1, 54, 0, 0, 1),
|
||||
(55, 1, 1, 55, 0, 0, 1),
|
||||
(56, 1, 1, 56, 0, 0, 1);
|
||||
@@ -39,91 +39,73 @@
|
||||
|
||||
<select id="countActiveTenants" resultType="java.lang.Long">
|
||||
SELECT COUNT(DISTINCT tenant_id)
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'LOGIN_COUNT'
|
||||
AND record_date >= CURRENT_DATE - INTERVAL '#{recentDays} days'
|
||||
FROM sys_login_log
|
||||
WHERE status = 1
|
||||
AND login_time >= CURRENT_DATE - (#{recentDays} - 1) * INTERVAL '1 day'
|
||||
AND deleted = FALSE
|
||||
</select>
|
||||
|
||||
<select id="getDailyActiveUsers" resultType="com.ljwx.platform.app.vo.billing.DailyStatVO">
|
||||
SELECT
|
||||
record_date AS date,
|
||||
SUM(usage_value)::BIGINT AS count
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'LOGIN_COUNT'
|
||||
AND record_date >= CURRENT_DATE - INTERVAL '#{recentDays} days'
|
||||
CAST(login_time AS DATE) AS date,
|
||||
COUNT(DISTINCT username)::BIGINT AS count
|
||||
FROM sys_login_log
|
||||
WHERE status = 1
|
||||
AND login_time >= CURRENT_DATE - (#{recentDays} - 1) * INTERVAL '1 day'
|
||||
AND deleted = FALSE
|
||||
GROUP BY record_date
|
||||
ORDER BY record_date DESC
|
||||
GROUP BY CAST(login_time AS DATE)
|
||||
ORDER BY CAST(login_time AS DATE) DESC
|
||||
</select>
|
||||
|
||||
<select id="sumStorageMb" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(usage_value), 0)
|
||||
FROM (
|
||||
SELECT DISTINCT ON (tenant_id) usage_value
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'STORAGE_MB'
|
||||
AND deleted = FALSE
|
||||
ORDER BY tenant_id, record_date DESC
|
||||
) latest_storage
|
||||
SELECT COALESCE(ROUND(SUM(file_size)::numeric / 1048576, 2), 0)
|
||||
FROM sys_file
|
||||
WHERE deleted = FALSE
|
||||
</select>
|
||||
|
||||
<select id="sumApiCallsToday" resultType="java.lang.Long">
|
||||
SELECT COALESCE(SUM(usage_value)::BIGINT, 0)
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'API_CALLS'
|
||||
AND record_date = CURRENT_DATE
|
||||
SELECT COUNT(*)::BIGINT
|
||||
FROM sys_operation_log
|
||||
WHERE created_time >= CURRENT_DATE
|
||||
AND created_time < CURRENT_DATE + INTERVAL '1 day'
|
||||
AND deleted = FALSE
|
||||
</select>
|
||||
|
||||
<select id="getTenantUsageSummaryList" resultType="com.ljwx.platform.app.vo.billing.TenantUsageSummaryVO">
|
||||
SELECT
|
||||
t.id AS tenantId,
|
||||
t.tenant_name AS tenantName,
|
||||
t.expire_time AS expireTime,
|
||||
t.name AS tenantName,
|
||||
NULL::TIMESTAMP AS expireTime,
|
||||
COALESCE(u.user_count, 0) AS userCount,
|
||||
COALESCE(s.storage_mb, 0) AS storageMb,
|
||||
COALESCE(a.api_calls_total, 0) AS apiCallsTotal,
|
||||
COALESCE(l.login_count_last30d, 0) AS loginCountLast30d,
|
||||
CASE WHEN t.expire_time IS NOT NULL AND t.expire_time <= CURRENT_TIMESTAMP + INTERVAL '30 days' THEN TRUE ELSE FALSE END AS isExpiringSoon
|
||||
FALSE AS isExpiringSoon
|
||||
FROM sys_tenant t
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id, usage_value AS user_count
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'USER_COUNT'
|
||||
AND deleted = FALSE
|
||||
AND (tenant_id, record_date) IN (
|
||||
SELECT tenant_id, MAX(record_date)
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'USER_COUNT' AND deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
)
|
||||
SELECT tenant_id, COUNT(*)::BIGINT AS user_count
|
||||
FROM sys_user
|
||||
WHERE deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
) u ON t.id = u.tenant_id
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id, usage_value AS storage_mb
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'STORAGE_MB'
|
||||
AND deleted = FALSE
|
||||
AND (tenant_id, record_date) IN (
|
||||
SELECT tenant_id, MAX(record_date)
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'STORAGE_MB' AND deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
)
|
||||
SELECT tenant_id, COALESCE(ROUND(SUM(file_size)::numeric / 1048576, 2), 0) AS storage_mb
|
||||
FROM sys_file
|
||||
WHERE deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
) s ON t.id = s.tenant_id
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id, SUM(usage_value)::BIGINT AS api_calls_total
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'API_CALLS'
|
||||
AND record_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
SELECT tenant_id, COUNT(*)::BIGINT AS api_calls_total
|
||||
FROM sys_operation_log
|
||||
WHERE created_time >= CURRENT_DATE - INTERVAL '29 days'
|
||||
AND deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
) a ON t.id = a.tenant_id
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id, SUM(usage_value)::BIGINT AS login_count_last30d
|
||||
FROM bill_usage_record
|
||||
WHERE metric_type = 'LOGIN_COUNT'
|
||||
AND record_date >= CURRENT_DATE - INTERVAL '30 days'
|
||||
SELECT tenant_id, COUNT(*)::BIGINT AS login_count_last30d
|
||||
FROM sys_login_log
|
||||
WHERE status = 1
|
||||
AND login_time >= CURRENT_DATE - INTERVAL '29 days'
|
||||
AND deleted = FALSE
|
||||
GROUP BY tenant_id
|
||||
) l ON t.id = l.tenant_id
|
||||
|
||||
@@ -101,4 +101,17 @@
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<update id="deleteById">
|
||||
UPDATE sys_operation_log
|
||||
SET deleted = TRUE
|
||||
WHERE id = #{id}
|
||||
AND deleted = FALSE
|
||||
</update>
|
||||
|
||||
<update id="cleanAll">
|
||||
UPDATE sys_operation_log
|
||||
SET deleted = TRUE
|
||||
WHERE deleted = FALSE
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
WHERE deleted = FALSE
|
||||
<if test="definitionId != null">AND definition_id = #{definitionId}</if>
|
||||
<if test="businessKey != null">AND business_key = #{businessKey}</if>
|
||||
<if test="businessType != null">AND business_type = #{businessType}</if>
|
||||
<if test="status != null">AND status = #{status}</if>
|
||||
ORDER BY created_time DESC
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
@@ -60,6 +61,7 @@
|
||||
WHERE deleted = FALSE
|
||||
<if test="definitionId != null">AND definition_id = #{definitionId}</if>
|
||||
<if test="businessKey != null">AND business_key = #{businessKey}</if>
|
||||
<if test="businessType != null">AND business_type = #{businessType}</if>
|
||||
<if test="status != null">AND status = #{status}</if>
|
||||
</select>
|
||||
|
||||
|
||||
+57
-11
@@ -2,6 +2,7 @@ package com.ljwx.platform.data.interceptor;
|
||||
|
||||
import com.ljwx.platform.core.context.CurrentTenantHolder;
|
||||
import com.ljwx.platform.core.context.CurrentUserHolder;
|
||||
import com.ljwx.platform.core.id.SnowflakeIdGenerator;
|
||||
import com.ljwx.platform.data.annotation.AuditChange;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -12,12 +13,11 @@ import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
@@ -48,6 +48,8 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
|
||||
private final ObjectProvider<CurrentTenantHolder> tenantHolderProvider;
|
||||
private final ObjectProvider<CurrentUserHolder> userHolderProvider;
|
||||
private final ObjectProvider<DataSource> dataSourceProvider;
|
||||
private final ObjectProvider<SnowflakeIdGenerator> idGeneratorProvider;
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
@@ -95,13 +97,18 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
);
|
||||
}
|
||||
|
||||
Long tenantId = resolveTenantId();
|
||||
Long userId = resolveUserId();
|
||||
|
||||
// 异步记录变更
|
||||
recordChangesAsync(
|
||||
annotation.tableName(),
|
||||
recordId,
|
||||
oldData,
|
||||
newData,
|
||||
sqlType == SqlCommandType.DELETE ? "DELETE" : "UPDATE"
|
||||
sqlType == SqlCommandType.DELETE ? "DELETE" : "UPDATE",
|
||||
tenantId,
|
||||
userId
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -165,7 +172,6 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
) {
|
||||
try {
|
||||
Executor executor = (Executor) invocation.getTarget();
|
||||
Configuration config = ((MappedStatement) invocation.getArgs()[0]).getConfiguration();
|
||||
|
||||
// 使用原生 JDBC 查询(避免触发其他拦截器)
|
||||
Connection conn = executor.getTransaction().getConnection();
|
||||
@@ -201,11 +207,11 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
Long recordId,
|
||||
Map<String, Object> oldData,
|
||||
Map<String, Object> newData,
|
||||
String operateType
|
||||
String operateType,
|
||||
Long tenantId,
|
||||
Long userId
|
||||
) {
|
||||
try {
|
||||
Long tenantId = resolveTenantId();
|
||||
Long userId = resolveUserId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<Map<String, Object>> changes = new ArrayList<>();
|
||||
@@ -217,7 +223,7 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
change.put("table_name", tableName);
|
||||
change.put("record_id", recordId);
|
||||
change.put("field_name", entry.getKey());
|
||||
change.put("old_value", String.valueOf(entry.getValue()));
|
||||
change.put("old_value", stringifyValue(entry.getValue()));
|
||||
change.put("new_value", "");
|
||||
change.put("operate_type", "DELETE");
|
||||
change.put("tenant_id", tenantId);
|
||||
@@ -237,8 +243,8 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
change.put("table_name", tableName);
|
||||
change.put("record_id", recordId);
|
||||
change.put("field_name", fieldName);
|
||||
change.put("old_value", String.valueOf(oldValue));
|
||||
change.put("new_value", String.valueOf(newValue));
|
||||
change.put("old_value", stringifyValue(oldValue));
|
||||
change.put("new_value", stringifyValue(newValue));
|
||||
change.put("operate_type", "UPDATE");
|
||||
change.put("tenant_id", tenantId);
|
||||
change.put("created_by", userId);
|
||||
@@ -249,7 +255,7 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
}
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
// TODO: 实际写入数据库(需要注入 SysDataChangeLogMapper)
|
||||
persistChanges(changes, now, userId);
|
||||
log.info("Recorded {} changes for {}.{}", changes.size(), tableName, recordId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -257,6 +263,46 @@ public class DataChangeInterceptor implements Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void persistChanges(List<Map<String, Object>> changes, LocalDateTime now, Long userId) throws Exception {
|
||||
DataSource dataSource = dataSourceProvider.getIfAvailable();
|
||||
SnowflakeIdGenerator idGenerator = idGeneratorProvider.getIfAvailable();
|
||||
if (dataSource == null || idGenerator == null) {
|
||||
log.warn("Data change audit skipped because datasource or id generator is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
String insertSql = "INSERT INTO sys_data_change_log (" +
|
||||
"id, table_name, record_id, field_name, old_value, new_value, operate_type, " +
|
||||
"tenant_id, created_by, created_time, updated_by, updated_time, deleted, version" +
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
var statement = connection.prepareStatement(insertSql)) {
|
||||
for (Map<String, Object> change : changes) {
|
||||
statement.setLong(1, idGenerator.nextId());
|
||||
statement.setString(2, (String) change.get("table_name"));
|
||||
statement.setLong(3, (Long) change.get("record_id"));
|
||||
statement.setString(4, (String) change.get("field_name"));
|
||||
statement.setString(5, (String) change.get("old_value"));
|
||||
statement.setString(6, (String) change.get("new_value"));
|
||||
statement.setString(7, (String) change.get("operate_type"));
|
||||
statement.setLong(8, (Long) change.get("tenant_id"));
|
||||
statement.setLong(9, (Long) change.get("created_by"));
|
||||
statement.setObject(10, change.get("created_time"));
|
||||
statement.setLong(11, userId);
|
||||
statement.setObject(12, now);
|
||||
statement.setBoolean(13, false);
|
||||
statement.setInt(14, 1);
|
||||
statement.addBatch();
|
||||
}
|
||||
statement.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
private String stringifyValue(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Long resolveTenantId() {
|
||||
try {
|
||||
CurrentTenantHolder holder = tenantHolderProvider.getIfAvailable();
|
||||
|
||||
@@ -2,9 +2,9 @@ import request from './request'
|
||||
import type { Result, UserVO } from '@ljwx/shared'
|
||||
|
||||
export function getUserProfile(): Promise<Result<UserVO>> {
|
||||
return request.get('/api/v1/users/profile')
|
||||
return request.get('/api/v1/profile')
|
||||
}
|
||||
|
||||
export function updateUserProfile(data: { nickname?: string; avatar?: string }): Promise<Result<void>> {
|
||||
return request.put('/api/v1/users/profile', data)
|
||||
return request.put('/api/v1/profile', data)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getScreenOverview, getScreenRealtime, getScreenTrend } from '@/api/screen'
|
||||
import type { ScreenOverviewVO, ScreenRealtimeVO, ScreenTrendVO } from '@ljwx/shared'
|
||||
import BarChart from '@/components/charts/BarChart.vue'
|
||||
@@ -17,48 +17,60 @@ const loading = ref(false)
|
||||
|
||||
let realtimeTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// ── Derived chart data ──────────────────────────────────────────────────────
|
||||
const trendXData = computed(() => trend.value?.userTrend.map((item) => item.date) ?? [])
|
||||
const userTrendData = computed(() => trend.value?.userTrend.map((item) => item.value) ?? [])
|
||||
const loginTrendData = computed(() => trend.value?.loginTrend.map((item) => item.value) ?? [])
|
||||
|
||||
const trendXData = ref<string[]>([])
|
||||
const userTrendData = ref<number[]>([])
|
||||
const loginTrendData = ref<number[]>([])
|
||||
const roleDistData = computed(() => overview.value?.roleDistribution ?? [])
|
||||
|
||||
const roleDistData = ref([
|
||||
{ name: '管理员', value: 12 },
|
||||
{ name: '普通用户', value: 88 },
|
||||
{ name: '访客', value: 24 },
|
||||
])
|
||||
|
||||
const tenantBarData = ref({
|
||||
xData: ['租户A', '租户B', '租户C', '租户D', '租户E'],
|
||||
series: [{ name: '用户数', data: [120, 85, 200, 60, 145] }],
|
||||
const tenantBarData = computed(() => {
|
||||
const distribution = overview.value?.tenantUserDistribution ?? []
|
||||
return {
|
||||
xData: distribution.map((item) => item.name),
|
||||
series: [{ name: '用户数', data: distribution.map((item) => item.value) }],
|
||||
}
|
||||
})
|
||||
|
||||
const userStatusData = computed(() => overview.value?.userStatusDistribution ?? [])
|
||||
|
||||
const scrollColumns = [
|
||||
{ key: 'username', label: '用户名', width: '120px' },
|
||||
{ key: 'action', label: '操作', width: '100px' },
|
||||
{ key: 'action', label: '操作', width: '180px' },
|
||||
{ key: 'time', label: '时间' },
|
||||
]
|
||||
|
||||
const scrollData = ref([
|
||||
{ username: 'admin', action: '登录', time: '10:01:23' },
|
||||
{ username: 'user01', action: '查询', time: '10:02:11' },
|
||||
{ username: 'user02', action: '新增', time: '10:03:05' },
|
||||
{ username: 'user03', action: '修改', time: '10:04:44' },
|
||||
{ username: 'user04', action: '删除', time: '10:05:30' },
|
||||
{ username: 'user05', action: '导出', time: '10:06:18' },
|
||||
{ username: 'user06', action: '登录', time: '10:07:02' },
|
||||
])
|
||||
const scrollData = computed(() => overview.value?.recentOperations ?? [])
|
||||
|
||||
const healthConfig = computed(() => {
|
||||
const metrics = [realtime.value?.cpuUsage ?? 0, realtime.value?.memoryUsage ?? 0]
|
||||
let normal = 0
|
||||
let warning = 0
|
||||
let abnormal = 0
|
||||
|
||||
metrics.forEach((metric) => {
|
||||
if (metric >= 90) {
|
||||
abnormal += 1
|
||||
} else if (metric >= 75) {
|
||||
warning += 1
|
||||
} else {
|
||||
normal += 1
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data: [
|
||||
{ name: '正常', value: normal },
|
||||
{ name: '警告', value: warning },
|
||||
{ name: '异常', value: abnormal },
|
||||
],
|
||||
radius: '60%',
|
||||
}
|
||||
})
|
||||
|
||||
// ── Data fetching ───────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchOverview() {
|
||||
loading.value = true
|
||||
try {
|
||||
overview.value = await getScreenOverview()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
overview.value = await getScreenOverview()
|
||||
}
|
||||
|
||||
async function fetchRealtime() {
|
||||
@@ -72,18 +84,22 @@ async function fetchRealtime() {
|
||||
async function fetchTrend() {
|
||||
try {
|
||||
trend.value = await getScreenTrend()
|
||||
if (trend.value) {
|
||||
trendXData.value = trend.value.userTrend.map((t) => t.date)
|
||||
userTrendData.value = trend.value.userTrend.map((t) => t.value)
|
||||
loginTrendData.value = trend.value.loginTrend.map((t) => t.value)
|
||||
}
|
||||
} catch {
|
||||
// silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScreen() {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([fetchOverview(), fetchRealtime(), fetchTrend()])
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchOverview(), fetchRealtime(), fetchTrend()])
|
||||
await loadScreen()
|
||||
realtimeTimer = setInterval(fetchRealtime, 5000)
|
||||
})
|
||||
|
||||
@@ -222,14 +238,7 @@ onUnmounted(() => {
|
||||
<!-- Pie chart -->
|
||||
<dv-border-box-8 class="panel">
|
||||
<div class="panel-title">用户状态分布</div>
|
||||
<PieChart
|
||||
:data="[
|
||||
{ name: '活跃', value: 68 },
|
||||
{ name: '非活跃', value: 22 },
|
||||
{ name: '禁用', value: 10 },
|
||||
]"
|
||||
height="200px"
|
||||
/>
|
||||
<PieChart :data="userStatusData" height="200px" />
|
||||
</dv-border-box-8>
|
||||
|
||||
<!-- Scroll table -->
|
||||
@@ -246,14 +255,7 @@ onUnmounted(() => {
|
||||
<dv-border-box-8 class="panel">
|
||||
<div class="panel-title">系统状态</div>
|
||||
<dv-active-ring-chart
|
||||
:config="{
|
||||
data: [
|
||||
{ name: '正常', value: 95 },
|
||||
{ name: '警告', value: 4 },
|
||||
{ name: '异常', value: 1 },
|
||||
],
|
||||
radius: '60%',
|
||||
}"
|
||||
:config="healthConfig"
|
||||
style="height: 180px"
|
||||
/>
|
||||
</dv-border-box-8>
|
||||
@@ -415,4 +417,3 @@ onUnmounted(() => {
|
||||
padding: 0 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3002,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const backendUrl = env.VITE_BACKEND_URL || 'http://localhost:8080'
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3002,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: backendUrl,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,6 +57,8 @@ export type {
|
||||
NoticeUpdateDTO,
|
||||
ScreenOverviewVO,
|
||||
ScreenRealtimeVO,
|
||||
ScreenStatItem,
|
||||
ScreenRecentOperation,
|
||||
ScreenTrendItem,
|
||||
ScreenTrendVO,
|
||||
MenuVO,
|
||||
|
||||
@@ -375,6 +375,14 @@ export interface ScreenOverviewVO {
|
||||
totalTenants: number
|
||||
/** 今日登录次数 */
|
||||
todayLoginCount: number
|
||||
/** 租户用户分布 */
|
||||
tenantUserDistribution: ScreenStatItem[]
|
||||
/** 角色分布 */
|
||||
roleDistribution: ScreenStatItem[]
|
||||
/** 用户状态分布 */
|
||||
userStatusDistribution: ScreenStatItem[]
|
||||
/** 最近操作日志 */
|
||||
recentOperations: ScreenRecentOperation[]
|
||||
}
|
||||
|
||||
export interface ScreenRealtimeVO {
|
||||
@@ -395,6 +403,17 @@ export interface ScreenTrendItem {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface ScreenStatItem {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface ScreenRecentOperation {
|
||||
username: string
|
||||
action: string
|
||||
time: string
|
||||
}
|
||||
|
||||
export interface ScreenTrendVO {
|
||||
/** 用户增长趋势(近 7 天) */
|
||||
userTrend: ScreenTrendItem[]
|
||||
@@ -449,4 +468,3 @@ export interface TaskLogStatsVO {
|
||||
/** 最小耗时(毫秒) */
|
||||
minDuration: number
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export type {
|
||||
NoticeUpdateDTO,
|
||||
ScreenOverviewVO,
|
||||
ScreenRealtimeVO,
|
||||
ScreenStatItem,
|
||||
ScreenRecentOperation,
|
||||
ScreenTrendItem,
|
||||
ScreenTrendVO,
|
||||
MenuVO,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# gate-admin-route-coverage.sh — fail when admin page views are not wired into router
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
ROUTER_FILE="ljwx-platform-admin/src/router/index.ts"
|
||||
ALLOWLIST_FILE="scripts/gates/admin-route-allowlist.txt"
|
||||
|
||||
if [[ ! -f "$ROUTER_FILE" ]]; then
|
||||
echo " gate-admin-route-coverage: SKIPPED (router file not found)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$ROUTER_FILE" "$ALLOWLIST_FILE" <<'PY'
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
router_file = pathlib.Path(sys.argv[1])
|
||||
allowlist_file = pathlib.Path(sys.argv[2])
|
||||
views_root = pathlib.Path("ljwx-platform-admin/src/views")
|
||||
|
||||
allowlist = set()
|
||||
if allowlist_file.exists():
|
||||
for line in allowlist_file.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
allowlist.add(line)
|
||||
|
||||
router_text = router_file.read_text(encoding="utf-8")
|
||||
routed = {
|
||||
match.replace("@/views/", "ljwx-platform-admin/src/views/")
|
||||
for match in re.findall(r"@/views/[^'\"`]+\.vue", router_text)
|
||||
}
|
||||
|
||||
candidates = []
|
||||
for file in sorted(views_root.rglob("*.vue")):
|
||||
rel = file.as_posix()
|
||||
if "/components/" in rel:
|
||||
continue
|
||||
if rel in allowlist:
|
||||
continue
|
||||
candidates.append(rel)
|
||||
|
||||
orphans = [path for path in candidates if path not in routed]
|
||||
|
||||
print(f"[Admin Route Coverage] routed={len(routed)} candidates={len(candidates)} orphans={len(orphans)}")
|
||||
|
||||
if orphans:
|
||||
for path in orphans:
|
||||
print(f" FAIL: orphan admin page view — {path}")
|
||||
print("")
|
||||
print("════════════════════════════════════════════════════")
|
||||
print(f" gate-admin-route-coverage: ERRORS={len(orphans)}")
|
||||
print(" gate-admin-route-coverage: FAILED")
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
print("════════════════════════════════════════════════════")
|
||||
print(" gate-admin-route-coverage: ERRORS=0")
|
||||
print(" gate-admin-route-coverage: PASSED")
|
||||
PY
|
||||
|
||||
@@ -183,6 +183,9 @@ fi
|
||||
echo ""
|
||||
|
||||
run_rule "R11" "Baseline" "scripts/gates/gate-baseline.sh" "false"
|
||||
run_rule "R12" "No Placeholder" "scripts/gates/gate-no-placeholder.sh" "false"
|
||||
run_rule "R13" "Frontend API Contract" "scripts/gates/gate-frontend-api-contract.sh" "false"
|
||||
run_rule "R14" "Admin Route Coverage" "scripts/gates/gate-admin-route-coverage.sh" "false"
|
||||
|
||||
if [[ -x "scripts/reports/collect-artifacts.sh" ]]; then
|
||||
bash scripts/reports/collect-artifacts.sh "$PHASE" >/dev/null 2>&1 || true
|
||||
|
||||
+169
-70
@@ -8,12 +8,15 @@
|
||||
# 3. 所有 API 路径都有对应的 operationId
|
||||
# 4. 仅在 app 模块存在且有 Controller 时执行
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
set -uo pipefail
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
ERRORS=0
|
||||
OPENAPI_BASE_URL="${OPENAPI_BASE_URL:-}"
|
||||
OPENAPI_FILE="${OPENAPI_FILE:-}"
|
||||
SYNC_OPENAPI_DOCS="${SYNC_OPENAPI_DOCS:-0}"
|
||||
|
||||
# ── Check: app module with controllers? ──
|
||||
APP_DIR="ljwx-platform-app"
|
||||
@@ -28,67 +31,66 @@ if [[ "$CONTROLLER_COUNT" -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Generate OpenAPI spec ──
|
||||
# Method 1: Use springdoc-openapi-maven-plugin if configured
|
||||
OPENAPI_OUTPUT="target/openapi.json"
|
||||
mkdir -p "$(dirname "$OPENAPI_OUTPUT")"
|
||||
|
||||
echo "[Contract] Generating OpenAPI spec"
|
||||
if grep -q 'springdoc-openapi-maven-plugin' pom.xml 2>/dev/null || \
|
||||
grep -rq 'springdoc-openapi-maven-plugin' "$APP_DIR/pom.xml" 2>/dev/null; then
|
||||
# Plugin-based generation
|
||||
if mvn verify -f pom.xml -B -DskipTests \
|
||||
-Dspringdoc.writer-with-order-by-keys=true 2>&1; then
|
||||
echo " OpenAPI spec generated via Maven plugin"
|
||||
if [[ -n "$OPENAPI_FILE" ]]; then
|
||||
if [[ -f "$OPENAPI_FILE" && -s "$OPENAPI_FILE" ]]; then
|
||||
cp "$OPENAPI_FILE" "$OPENAPI_OUTPUT"
|
||||
echo " OpenAPI spec loaded from $OPENAPI_FILE"
|
||||
else
|
||||
echo " WARN: Maven plugin generation failed, trying runtime generation"
|
||||
echo " FAIL: OPENAPI_FILE not found or empty — $OPENAPI_FILE"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 2: Start app briefly and fetch from /v3/api-docs
|
||||
if [[ ! -s "$OPENAPI_OUTPUT" ]]; then
|
||||
echo " Attempting runtime OpenAPI generation"
|
||||
|
||||
# Ensure DB is up (using local PostgreSQL)
|
||||
echo " Checking local PostgreSQL connection"
|
||||
if command -v psql > /dev/null 2>&1; then
|
||||
if ! psql -U postgres -d ljwx_platform -c "SELECT 1" > /dev/null 2>&1; then
|
||||
echo " WARNING: Cannot connect to local PostgreSQL"
|
||||
elif [[ -n "$OPENAPI_BASE_URL" ]]; then
|
||||
if curl -fsS --retry 5 --retry-delay 1 "${OPENAPI_BASE_URL%/}/v3/api-docs" > "$OPENAPI_OUTPUT"; then
|
||||
echo " OpenAPI spec fetched from $OPENAPI_BASE_URL"
|
||||
else
|
||||
echo " FAIL: Could not fetch OpenAPI spec from $OPENAPI_BASE_URL"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
if grep -q 'springdoc-openapi-maven-plugin' pom.xml 2>/dev/null || \
|
||||
grep -rq 'springdoc-openapi-maven-plugin' "$APP_DIR/pom.xml" 2>/dev/null; then
|
||||
if mvn verify -f pom.xml -B -DskipTests \
|
||||
-Dspringdoc.writer-with-order-by-keys=true 2>&1; then
|
||||
echo " OpenAPI spec generated via Maven plugin"
|
||||
else
|
||||
echo " WARN: Maven plugin generation failed, trying runtime generation"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -s "$OPENAPI_OUTPUT" ]]; then
|
||||
echo " Attempting runtime OpenAPI generation"
|
||||
mvn package -f pom.xml -B -DskipTests -q 2>/dev/null
|
||||
APP_JAR=$(find "$APP_DIR/target" -name '*.jar' ! -name '*-sources.jar' 2>/dev/null | head -1)
|
||||
|
||||
# Build and start app in background
|
||||
mvn package -f pom.xml -B -DskipTests -q 2>/dev/null
|
||||
APP_JAR=$(find "$APP_DIR/target" -name '*.jar' ! -name '*-sources.jar' 2>/dev/null | head -1)
|
||||
if [[ -n "$APP_JAR" && -f "$APP_JAR" ]]; then
|
||||
java -jar "$APP_JAR" \
|
||||
--server.port=18080 \
|
||||
--cache.enabled=false \
|
||||
--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration &
|
||||
APP_PID=$!
|
||||
|
||||
if [[ -n "$APP_JAR" && -f "$APP_JAR" ]]; then
|
||||
# Runtime OpenAPI extraction should not depend on local Redis auth state.
|
||||
java -jar "$APP_JAR" \
|
||||
--server.port=18080 \
|
||||
--cache.enabled=false \
|
||||
--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration &
|
||||
APP_PID=$!
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:18080/v3/api-docs > "$OPENAPI_OUTPUT" 2>/dev/null; then
|
||||
echo " OpenAPI spec fetched from running application"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait for startup (max 60s)
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:18080/v3/api-docs > "$OPENAPI_OUTPUT" 2>/dev/null; then
|
||||
echo " OpenAPI spec fetched from running application"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
kill "$APP_PID" 2>/dev/null || true
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
kill "$APP_PID" 2>/dev/null || true
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Validate generated spec ──
|
||||
if [[ ! -f "$OPENAPI_OUTPUT" || ! -s "$OPENAPI_OUTPUT" ]]; then
|
||||
echo " WARN: Could not generate OpenAPI spec — skipping contract validation"
|
||||
echo " gate-contract: SKIPPED"
|
||||
exit 0
|
||||
echo " FAIL: Could not generate OpenAPI spec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -98,43 +100,140 @@ echo "[Contract] Validating OpenAPI spec"
|
||||
if ! python3 -c "import json; json.load(open('$OPENAPI_OUTPUT'))" 2>/dev/null && \
|
||||
! jq empty "$OPENAPI_OUTPUT" 2>/dev/null; then
|
||||
echo " FAIL: $OPENAPI_OUTPUT is not valid JSON"
|
||||
((ERRORS++))
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check: expected API paths from spec/03-api.md
|
||||
MISSING_OPERATION_IDS="$(jq -r '
|
||||
.paths
|
||||
| to_entries[]
|
||||
| .key as $path
|
||||
| .value
|
||||
| to_entries[]
|
||||
| select(.value.operationId == null or .value.operationId == "")
|
||||
| "\(.key | ascii_upcase) \($path)"
|
||||
' "$OPENAPI_OUTPUT" 2>/dev/null || true)"
|
||||
if [[ -n "$MISSING_OPERATION_IDS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
[[ -z "$hit" ]] && continue
|
||||
echo " FAIL: Missing operationId for $hit"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$MISSING_OPERATION_IDS"
|
||||
fi
|
||||
|
||||
# Check: expected API routes from spec/03-api.md
|
||||
if [[ -f "spec/03-api.md" ]]; then
|
||||
echo "[Contract] Cross-checking with spec/03-api.md"
|
||||
# Extract expected paths from api spec (lines like "POST /api/v1/auth/login")
|
||||
EXPECTED_PATHS=$(grep -oE '(GET|POST|PUT|DELETE|PATCH)[[:space:]]+/api/[^[:space:]]+' spec/03-api.md \
|
||||
| awk '{print $2}' | sort -u || true)
|
||||
EXPECTED_ROUTES="$(
|
||||
python3 - "spec/03-api.md" <<'PY'
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
if [[ -n "$EXPECTED_PATHS" ]]; then
|
||||
# Extract actual paths from OpenAPI JSON
|
||||
ACTUAL_PATHS=$(jq -r '.paths | keys[]' "$OPENAPI_OUTPUT" 2>/dev/null | sort -u || true)
|
||||
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
routes = set()
|
||||
|
||||
MISSING_COUNT=0
|
||||
while IFS= read -r expected; do
|
||||
[[ -z "$expected" ]] && continue
|
||||
# Normalize path parameters: /api/v1/users/{id} → match with regex
|
||||
PATTERN=$(echo "$expected" | sed 's/{[^}]*}/[^\/]*/g')
|
||||
if ! echo "$ACTUAL_PATHS" | grep -qE "^${PATTERN}$"; then
|
||||
echo " WARN: Expected path $expected not found in generated spec"
|
||||
((MISSING_COUNT++))
|
||||
table_pattern = re.compile(r"\|\s*(GET|POST|PUT|DELETE|PATCH)\s*\|\s*(/api/[^\s|]+)\s*\|", re.IGNORECASE)
|
||||
inline_pattern = re.compile(r"\b(GET|POST|PUT|DELETE|PATCH)\s+(/api/\S+)", re.IGNORECASE)
|
||||
|
||||
for method, path in table_pattern.findall(text):
|
||||
routes.add((method.upper(), path))
|
||||
|
||||
for method, path in inline_pattern.findall(text):
|
||||
routes.add((method.upper(), path.rstrip("`|),.")))
|
||||
|
||||
for method, path in sorted(routes):
|
||||
print(f"{method}\t{path}")
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ -n "$EXPECTED_ROUTES" ]]; then
|
||||
SPEC_ERRORS=0
|
||||
while IFS=$'\t' read -r expected_method expected_path; do
|
||||
[[ -z "$expected_method" || -z "$expected_path" ]] && continue
|
||||
|
||||
canonical_expected="$(echo "$expected_path" | sed -E 's/\{[^}]+\}/{param}/g')"
|
||||
method_key="$(echo "$expected_method" | tr '[:upper:]' '[:lower:]')"
|
||||
matched_path="$(
|
||||
jq -r \
|
||||
--arg expected_path "$expected_path" \
|
||||
--arg canonical_expected "$canonical_expected" \
|
||||
--arg method "$method_key" '
|
||||
if .paths[$expected_path] != null then
|
||||
$expected_path
|
||||
else
|
||||
(
|
||||
[
|
||||
.paths
|
||||
| to_entries[]
|
||||
| select((.key | gsub("\\{[^}]+\\}"; "{param}")) == $canonical_expected and .value[$method] != null)
|
||||
| .key
|
||||
][0]
|
||||
) // (
|
||||
[
|
||||
.paths
|
||||
| keys[]
|
||||
| select((gsub("\\{[^}]+\\}"; "{param}")) == $canonical_expected)
|
||||
][0]
|
||||
) // ""
|
||||
end
|
||||
' "$OPENAPI_OUTPUT" 2>/dev/null
|
||||
)"
|
||||
|
||||
if [[ -z "$matched_path" ]]; then
|
||||
echo " FAIL: Expected route $expected_method $expected_path not found in generated spec"
|
||||
SPEC_ERRORS=$((SPEC_ERRORS + 1))
|
||||
continue
|
||||
fi
|
||||
done <<< "$EXPECTED_PATHS"
|
||||
|
||||
if [[ $MISSING_COUNT -gt 0 ]]; then
|
||||
echo " WARNING: $MISSING_COUNT expected paths not found in OpenAPI spec"
|
||||
# This is a warning, not an error — paths may not be implemented yet
|
||||
if ! jq -e --arg path "$matched_path" --arg method "$method_key" '.paths[$path][$method] != null' "$OPENAPI_OUTPUT" >/dev/null 2>&1; then
|
||||
actual_methods="$(jq -r --arg path "$matched_path" '.paths[$path] | keys | map(ascii_upcase) | join(", ")' "$OPENAPI_OUTPUT" 2>/dev/null || true)"
|
||||
echo " FAIL: Expected route $expected_method $expected_path not exported (available methods: ${actual_methods:-none})"
|
||||
SPEC_ERRORS=$((SPEC_ERRORS + 1))
|
||||
fi
|
||||
done <<< "$EXPECTED_ROUTES"
|
||||
|
||||
if [[ $SPEC_ERRORS -gt 0 ]]; then
|
||||
echo " FAIL: $SPEC_ERRORS spec routes not satisfied by generated OpenAPI"
|
||||
ERRORS=$((ERRORS + SPEC_ERRORS))
|
||||
else
|
||||
echo " PASS: All expected API paths found"
|
||||
echo " PASS: All expected API routes found"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Copy to docs for reference ──
|
||||
mkdir -p docs/api
|
||||
cp "$OPENAPI_OUTPUT" docs/api/openapi.json 2>/dev/null || true
|
||||
if [[ -f "docs/api/openapi.json" ]]; then
|
||||
ACTUAL_PATHS_FILE="$(mktemp)"
|
||||
REPO_PATHS_FILE="$(mktemp)"
|
||||
jq -r '.paths | keys[]' "$OPENAPI_OUTPUT" | sort -u >"$ACTUAL_PATHS_FILE"
|
||||
jq -r '.paths | keys[]' "docs/api/openapi.json" | sort -u >"$REPO_PATHS_FILE"
|
||||
|
||||
RUNTIME_ONLY="$(comm -23 "$ACTUAL_PATHS_FILE" "$REPO_PATHS_FILE" || true)"
|
||||
REPO_ONLY="$(comm -13 "$ACTUAL_PATHS_FILE" "$REPO_PATHS_FILE" || true)"
|
||||
|
||||
if [[ -n "$RUNTIME_ONLY" || -n "$REPO_ONLY" ]]; then
|
||||
echo "[Contract] Checking docs/api/openapi.json drift"
|
||||
if [[ -n "$RUNTIME_ONLY" ]]; then
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
echo " FAIL: runtime-only path missing from docs/api/openapi.json — $path"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$RUNTIME_ONLY"
|
||||
fi
|
||||
if [[ -n "$REPO_ONLY" ]]; then
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
echo " FAIL: stale docs/api/openapi.json path not exported at runtime — $path"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$REPO_ONLY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SYNC_OPENAPI_DOCS" == "1" ]]; then
|
||||
mkdir -p docs/api
|
||||
cp "$OPENAPI_OUTPUT" docs/api/openapi.json
|
||||
echo " Synced docs/api/openapi.json from generated spec"
|
||||
fi
|
||||
|
||||
# ── Summary ──
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# gate-frontend-api-contract.sh — ensure admin/screen/mobile API clients only call exported OpenAPI paths
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
OPENAPI_FILE="${OPENAPI_FILE:-target/openapi.json}"
|
||||
ALLOWLIST_FILE="scripts/gates/frontend-api-allowlist.txt"
|
||||
|
||||
if [[ ! -f "$OPENAPI_FILE" || ! -s "$OPENAPI_FILE" ]]; then
|
||||
OPENAPI_FILE="docs/api/openapi.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$OPENAPI_FILE" || ! -s "$OPENAPI_FILE" ]]; then
|
||||
echo " FAIL: OpenAPI file not found. Run gate-contract first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[Frontend API Contract] Using $OPENAPI_FILE"
|
||||
|
||||
python3 - "$OPENAPI_FILE" "$ALLOWLIST_FILE" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
openapi_file = pathlib.Path(sys.argv[1])
|
||||
allowlist_file = pathlib.Path(sys.argv[2])
|
||||
|
||||
with open(openapi_file, "r", encoding="utf-8") as fh:
|
||||
spec = json.load(fh)
|
||||
|
||||
openapi_methods = {}
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
canonical = re.sub(r"\{[^}]+\}", "{param}", path)
|
||||
openapi_methods.setdefault(canonical, set()).update(m.lower() for m in methods.keys())
|
||||
|
||||
allowlist = set()
|
||||
if allowlist_file.exists():
|
||||
for line in allowlist_file.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
allowlist.add(line)
|
||||
|
||||
roots = [
|
||||
pathlib.Path("ljwx-platform-admin/src/api"),
|
||||
pathlib.Path("ljwx-platform-screen/src/api"),
|
||||
pathlib.Path("ljwx-platform-mobile/src/api"),
|
||||
]
|
||||
|
||||
call_re = re.compile(
|
||||
r"request\.(get|post|put|delete|patch)(?:<[^>]+>)?\(\s*(?:`([^`]+)`|'([^']+)'|\"([^\"]+)\")"
|
||||
)
|
||||
|
||||
errors = []
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
for file in sorted(root.rglob("*.ts")):
|
||||
for lineno, line in enumerate(file.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
for match in call_re.finditer(line):
|
||||
method = match.group(1).lower()
|
||||
path = next(group for group in match.groups()[1:] if group is not None)
|
||||
if not path.startswith("/api/"):
|
||||
continue
|
||||
canonical = re.sub(r"\$\{[^}]+\}", "{param}", path)
|
||||
allow_key = f"{method} {canonical}"
|
||||
if allow_key in allowlist or canonical in allowlist:
|
||||
continue
|
||||
supported = openapi_methods.get(canonical)
|
||||
if supported is None:
|
||||
errors.append(f"{file}:{lineno} {method.upper()} {path} — path not found in OpenAPI")
|
||||
elif method not in supported:
|
||||
errors.append(
|
||||
f"{file}:{lineno} {method.upper()} {path} — path exists but method not exported ({sorted(supported)})"
|
||||
)
|
||||
|
||||
if errors:
|
||||
for issue in errors:
|
||||
print(f" FAIL: {issue}")
|
||||
print("")
|
||||
print("════════════════════════════════════════════════════")
|
||||
print(f" gate-frontend-api-contract: ERRORS={len(errors)}")
|
||||
print(" gate-frontend-api-contract: FAILED")
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
print("════════════════════════════════════════════════════")
|
||||
print(" gate-frontend-api-contract: ERRORS=0")
|
||||
print(" gate-frontend-api-contract: PASSED")
|
||||
PY
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# gate-no-placeholder.sh — fail on fake-complete implementation markers
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
ERRORS=0
|
||||
ALLOWLIST_FILE="scripts/gates/placeholder-allowlist.txt"
|
||||
PATTERN='\bTODO\b|\bFIXME\b|占位(响应|数据|实现)|简化实现|实际应(调用|使用|写入|查询|获取)|Implement actual|Encrypt in production|硬编码 AES 密钥|生产环境应从配置中心获取|example\.com/exports'
|
||||
|
||||
TARGETS=(
|
||||
"ljwx-platform-app/src/main/java"
|
||||
"ljwx-platform-core/src/main/java"
|
||||
"ljwx-platform-data/src/main/java"
|
||||
"ljwx-platform-security/src/main/java"
|
||||
"ljwx-platform-web/src/main/java"
|
||||
"ljwx-platform-admin/src"
|
||||
"ljwx-platform-screen/src"
|
||||
"ljwx-platform-mobile/src"
|
||||
"packages/shared/src"
|
||||
)
|
||||
|
||||
GLOBS=(
|
||||
"--glob=*.java"
|
||||
"--glob=*.kt"
|
||||
"--glob=*.js"
|
||||
"--glob=*.jsx"
|
||||
"--glob=*.ts"
|
||||
"--glob=*.tsx"
|
||||
"--glob=*.vue"
|
||||
)
|
||||
|
||||
if ! command -v rg >/dev/null 2>&1; then
|
||||
echo " FAIL: rg is required for gate-no-placeholder.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[Fake Complete] Scanning for fake-complete markers"
|
||||
|
||||
hits="$(
|
||||
rg -n -S \
|
||||
"${GLOBS[@]}" \
|
||||
--glob '!**/node_modules/**' \
|
||||
--glob '!**/target/**' \
|
||||
--glob '!**/dist/**' \
|
||||
--glob '!**/.pnpm/**' \
|
||||
--glob '!**/auto-imports.d.ts' \
|
||||
--glob '!**/components.d.ts' \
|
||||
"$PATTERN" \
|
||||
"${TARGETS[@]}" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
if [[ -f "$ALLOWLIST_FILE" ]]; then
|
||||
hits="$(printf '%s\n' "$hits" | grep -F -v -f "$ALLOWLIST_FILE" || true)"
|
||||
fi
|
||||
|
||||
if [[ -n "$hits" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
[[ -z "$hit" ]] && continue
|
||||
echo " FAIL: fake-complete marker found — $hit"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
done <<< "$hits"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " gate-no-placeholder: ERRORS=$ERRORS"
|
||||
if [[ $ERRORS -gt 0 ]]; then
|
||||
echo " gate-no-placeholder: FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo " gate-no-placeholder: PASSED"
|
||||
@@ -116,6 +116,9 @@ SCRIPT_FILES=(
|
||||
"scripts/gates/gate-integration.sh"
|
||||
"scripts/gates/gate-contract.sh"
|
||||
"scripts/gates/gate-nfr.sh"
|
||||
"scripts/gates/gate-no-placeholder.sh"
|
||||
"scripts/gates/gate-frontend-api-contract.sh"
|
||||
"scripts/gates/gate-admin-route-coverage.sh"
|
||||
"scripts/hooks/pre-edit-guard.sh"
|
||||
"scripts/hooks/post-edit-check.sh"
|
||||
"scripts/hooks/stop-gate.sh"
|
||||
@@ -588,6 +591,9 @@ ALL_SCRIPTS=(
|
||||
"scripts/gates/gate-integration.sh"
|
||||
"scripts/gates/gate-contract.sh"
|
||||
"scripts/gates/gate-nfr.sh"
|
||||
"scripts/gates/gate-no-placeholder.sh"
|
||||
"scripts/gates/gate-frontend-api-contract.sh"
|
||||
"scripts/gates/gate-admin-route-coverage.sh"
|
||||
"scripts/hooks/pre-edit-guard.sh"
|
||||
"scripts/hooks/post-edit-check.sh"
|
||||
"scripts/hooks/stop-gate.sh"
|
||||
|
||||
Reference in New Issue
Block a user