版本升级
1. 新增:微信小程序端新增微信手机号登录功能(必须为企业认证小程序) 2. 新增:加入动态更新常见问题 3. 新增:新增小程序分享功能 4. 新增:小程序新增第一次登录需要修改密码 5. 新增:新增接口权限控制 6. 新增:用户新增is_staff用来判断是否为工作人员 7. 新增:软删除新增is_delete字段来判断,delete_datetime当前主要来记录时间 8. 更新:部分接口删除功能已更新,需要试用软删除的才会试用软删除 9. 更新:更新系统配置缓存功能 10. 更新:接口认证依赖项更新 11. 更新:获取系统基础配置信息与用户协议与隐私协议更新 12. 优化:优化接口与数据库操作
This commit is contained in:
parent
7c1723c9d2
commit
7fbdcb3b0f
19
README.md
19
README.md
@ -55,8 +55,6 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
|
||||
|
||||
PC端演示地址:http://kinit.ktianc.top
|
||||
|
||||
移动端演示地址:http://h5.ktianc.top
|
||||
|
||||
微信小程序端演示:
|
||||
|
||||
- 搜索:kinit
|
||||
@ -87,7 +85,7 @@ github地址:https://github.com/vvandk/kinit
|
||||
|
||||
- [x] 文件上传:对接阿里云OSS与本地存储。
|
||||
|
||||
- [x] 登录认证:目前支持用户使用手机号+密码方式或者手机验证码登录。
|
||||
- [x] 登录认证:目前支持用户使用手机号+密码登录方式,手机验证码登录方式。
|
||||
|
||||
说明:新建用户密码默认为手机号后六位;
|
||||
|
||||
@ -95,16 +93,16 @@ github地址:https://github.com/vvandk/kinit
|
||||
|
||||
- [x] 系统配置:对本系统环境信息进行动态配置
|
||||
|
||||
网站标题,LOGO,描述,ICO,备案号,底部内容,百度统计代码,等等
|
||||
网站标题,LOGO,描述,ICO,备案号,底部内容,微信小程序信息,等等
|
||||
|
||||
- [x] 用户分布:接入高德地图显示各地区用户分布情况
|
||||
|
||||
- [x] 智慧大屏:大屏展示`办公室空气质量实时检测`数据分析
|
||||
|
||||
- [x] 登录日志:用户登录日志记录和查询。
|
||||
|
||||
- [x] 操作日志:系统用户每次操作功能时的详细记录。
|
||||
|
||||
- [ ] **异常日志:获取并展示接口异常日志**
|
||||
|
||||
- [x] 接口文档:提供自动生成的交互式 API 文档,与 ReDoc 文档
|
||||
|
||||
- [x] 导入导出:灵活支持数据导入导出功能
|
||||
@ -123,7 +121,7 @@ github地址:https://github.com/vvandk/kinit
|
||||
|
||||
## 移动端内置功能
|
||||
|
||||
- [x] 登录认证:目前支持用户使用手机号+密码方式登录。
|
||||
- [x] 登录认证:支持用户使用手机号+密码方式登录,微信手机号一键登录方式。
|
||||
|
||||
说明:新建用户密码默认为手机号后六位;
|
||||
|
||||
@ -138,7 +136,6 @@ github地址:https://github.com/vvandk/kinit
|
||||
- [ ] 考虑支持多机部署方案,如果接口使用多机,那么用户是否支持统一认证
|
||||
- [ ] **自动化编排服务:使用docker-compose部署项目**
|
||||
- [ ] **数据库备份:自动备份数据库**
|
||||
- [ ] **接入数据大屏**
|
||||
- [ ] **可视化低代码表单:接入低代码表单,https://vform666.com/vform3.html?from=element_plus**
|
||||
|
||||
## 前序准备
|
||||
@ -315,6 +312,10 @@ Redis (推荐使用最新稳定版)
|
||||
|
||||
# 高德地图配置
|
||||
map_key
|
||||
|
||||
# 微信小程序配置
|
||||
wx_server_app_id
|
||||
wx_server_app_secret
|
||||
```
|
||||
|
||||
6. 启动
|
||||
@ -402,6 +403,8 @@ pnpm run build:pro
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
BIN
images/10.png
Normal file
BIN
images/10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 602 KiB |
@ -5,7 +5,7 @@ import { ConfigGlobal } from '@/components/ConfigGlobal'
|
||||
import { isDark } from '@/utils/is'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { getSystemSettingsClassifysApi } from '@/api/vadmin/system/settings'
|
||||
import { getSystemBaseConfigApi } from '@/api/vadmin/system/settings'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@ -23,16 +23,15 @@ const addMeta = (name: string, content: string) => {
|
||||
|
||||
// 获取并设置系统配置
|
||||
const setSystemConfig = async () => {
|
||||
const res = await getSystemSettingsClassifysApi({ classify: 'web' })
|
||||
const res = await getSystemBaseConfigApi()
|
||||
if (res) {
|
||||
appStore.setTitle(res.data.web_basic.web_title || import.meta.env.VITE_APP_TITLE)
|
||||
appStore.setLogoImage(res.data.web_basic.web_logo || '/media/system/logo.png')
|
||||
appStore.setFooterContent(res.data.web_basic.web_copyright || 'Copyright ©2022-present K')
|
||||
appStore.setIcpNumber(res.data.web_basic.web_icp_number || '')
|
||||
appStore.setTitle(res.data.web_title || import.meta.env.VITE_APP_TITLE)
|
||||
appStore.setLogoImage(res.data.web_logo || '/media/system/logo.png')
|
||||
appStore.setFooterContent(res.data.web_copyright || 'Copyright ©2022-present K')
|
||||
appStore.setIcpNumber(res.data.web_icp_number || '')
|
||||
addMeta(
|
||||
'description',
|
||||
res.data.web_basic.web_desc ||
|
||||
'Kinit 是一套开箱即用的中后台解决方案,可以作为新项目的启动模版。'
|
||||
res.data.web_desc || 'Kinit 是一套开箱即用的中后台解决方案,可以作为新项目的启动模版。'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ export const postCurrentUserUpdateInfo = (data: any): Promise<IResponse> => {
|
||||
return request.post({ url: `/vadmin/auth/user/current/update/info/`, data })
|
||||
}
|
||||
|
||||
export const getCurrentUserInfo = (): Promise<IResponse> => {
|
||||
return request.get({ url: `/vadmin/auth/user/current/info/` })
|
||||
export const getCurrentAdminUserInfo = (): Promise<IResponse> => {
|
||||
return request.get({ url: `/vadmin/auth/user/admin/current/info/` })
|
||||
}
|
||||
|
||||
export const postExportUserQueryListApi = (params: any, data: any): Promise<IResponse> => {
|
||||
@ -43,10 +43,7 @@ export const getImportTemplateApi = (): Promise<IResponse> => {
|
||||
export const postImportUserApi = (data: any): Promise<IResponse> => {
|
||||
return request.post({
|
||||
url: `/vadmin/auth/import/users/`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 5 * 60 * 1000,
|
||||
headersType: 'multipart/form-data',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
47
kinit-admin/src/api/vadmin/help/issue.ts
Normal file
47
kinit-admin/src/api/vadmin/help/issue.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// 常见问题类别
|
||||
export const getIssueCategoryListApi = (params: any): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/help/issue/categorys/', params })
|
||||
}
|
||||
|
||||
export const addIssueCategoryApi = (data: any): Promise<IResponse> => {
|
||||
return request.post({ url: '/vadmin/help/issue/categorys/', data })
|
||||
}
|
||||
|
||||
export const delIssueCategoryListApi = (data: any): Promise<IResponse> => {
|
||||
return request.delete({ url: '/vadmin/help/issue/categorys/', data })
|
||||
}
|
||||
|
||||
export const putIssueCategoryApi = (data: any): Promise<IResponse> => {
|
||||
return request.put({ url: `/vadmin/help/issue/categorys/${data.id}/`, data })
|
||||
}
|
||||
|
||||
export const getIssueCategoryApi = (dataId: number): Promise<IResponse> => {
|
||||
return request.get({ url: `/vadmin/help/issue/categorys/${dataId}/` })
|
||||
}
|
||||
|
||||
export const getIssueCategoryOptionsApi = (): Promise<IResponse> => {
|
||||
return request.get({ url: `/vadmin/help/issue/categorys/options/` })
|
||||
}
|
||||
|
||||
// 常见问题
|
||||
export const getIssueListApi = (params: any): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/help/issues/', params })
|
||||
}
|
||||
|
||||
export const addIssueApi = (data: any): Promise<IResponse> => {
|
||||
return request.post({ url: '/vadmin/help/issues/', data })
|
||||
}
|
||||
|
||||
export const delIssueListApi = (data: any): Promise<IResponse> => {
|
||||
return request.delete({ url: '/vadmin/help/issues/', data })
|
||||
}
|
||||
|
||||
export const putIssueApi = (data: any): Promise<IResponse> => {
|
||||
return request.put({ url: `/vadmin/help/issues/${data.id}/`, data })
|
||||
}
|
||||
|
||||
export const getIssueApi = (dataId: number): Promise<IResponse> => {
|
||||
return request.get({ url: `/vadmin/help/issues/${dataId}/` })
|
||||
}
|
9
kinit-admin/src/api/vadmin/system/files.ts
Normal file
9
kinit-admin/src/api/vadmin/system/files.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export const addFilesListApi = (data: any): Promise<IResponse> => {
|
||||
return request.post({
|
||||
url: `/vadmin/system/files/`,
|
||||
headersType: 'multipart/form-data',
|
||||
data
|
||||
})
|
||||
}
|
@ -12,10 +12,17 @@ export const putSystemSettingsApi = (data: any): Promise<IResponse> => {
|
||||
return request.put({ url: '/vadmin/system/settings/tabs/values/', data })
|
||||
}
|
||||
|
||||
export const getSystemSettingsClassifysApi = (params: any): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/system/settings/classifys/', params })
|
||||
// 获取系统基础配置,每次进入系统时使用
|
||||
export const getSystemBaseConfigApi = (): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/system/settings/base/config/' })
|
||||
}
|
||||
|
||||
export const getSystemSettingsConfigValueApi = (params: any): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/system/settings/config/value/', params })
|
||||
// 获取系统隐私协议
|
||||
export const getSystemPrivacyApi = (): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/system/settings/privacy/' })
|
||||
}
|
||||
|
||||
// 获取系统用户协议
|
||||
export const getSystemAgreementApi = (): Promise<IResponse> => {
|
||||
return request.get({ url: '/vadmin/system/settings/agreement/' })
|
||||
}
|
||||
|
@ -63,7 +63,6 @@ const dialogStyle = computed(() => {
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
draggable
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #header>
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
ElTree
|
||||
} from 'element-plus'
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { Text } from '@/components/Text'
|
||||
import { ComponentName } from '@/types/components'
|
||||
|
||||
@ -46,7 +45,6 @@ const componentMap: Recordable<Component, ComponentName> = {
|
||||
SelectV2: ElSelectV2,
|
||||
RadioButton: ElRadioGroup,
|
||||
InputPassword: InputPassword,
|
||||
Editor: Editor,
|
||||
TreeSelect: ElTreeSelect,
|
||||
Tree: ElTree,
|
||||
Text: Text
|
||||
|
@ -3,7 +3,6 @@ const config: {
|
||||
unauthorized_code: number | string
|
||||
default_headers: AxiosHeaders
|
||||
request_timeout: number
|
||||
token: string
|
||||
} = {
|
||||
/**
|
||||
* 接口成功返回状态码
|
||||
@ -23,13 +22,7 @@ const config: {
|
||||
* 默认接口请求类型
|
||||
* 可选值:application/x-www-form-urlencoded multipart/form-data
|
||||
*/
|
||||
default_headers: 'application/json',
|
||||
|
||||
/**
|
||||
* 存储Token字段
|
||||
* 关联 config/axios/service/service.interceptors
|
||||
*/
|
||||
token: 'Token'
|
||||
default_headers: 'application/json'
|
||||
}
|
||||
|
||||
export { config }
|
||||
|
@ -1,11 +1,12 @@
|
||||
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useAuthStore } from '@/store/modules/auth'
|
||||
import qs from 'qs'
|
||||
import { config } from './config'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/store/modules/auth'
|
||||
|
||||
const { result_code, unauthorized_code, request_timeout, token } = config
|
||||
const { result_code, unauthorized_code, request_timeout } = config
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
@ -19,9 +20,10 @@ const service: AxiosInstance = axios.create({
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const _token = wsCache.get(token)
|
||||
if (_token !== '') {
|
||||
;(config.headers as any)['Authorization'] = _token // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
const appStore = useAppStore()
|
||||
const token = wsCache.get(appStore.getToken)
|
||||
if (token !== '') {
|
||||
;(config.headers as any)['Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||
}
|
||||
if (
|
||||
config.method === 'post' &&
|
||||
@ -35,7 +37,12 @@ service.interceptors.request.use(
|
||||
url += '?'
|
||||
const keys = Object.keys(config.params)
|
||||
for (const key of keys) {
|
||||
if (config.params[key] !== void 0 && config.params[key] !== null) {
|
||||
if (
|
||||
// 禁止提交的get参数类型
|
||||
config.params[key] !== void 0 &&
|
||||
config.params[key] !== null &&
|
||||
config.params[key] !== ''
|
||||
) {
|
||||
url += `${key}=${encodeURIComponent(config.params[key])}&`
|
||||
}
|
||||
}
|
||||
@ -73,11 +80,11 @@ service.interceptors.response.use(
|
||||
console.log('err' + error)
|
||||
let { message } = error
|
||||
if (message == 'Network Error') {
|
||||
message = '系统接口连接异常'
|
||||
message = '后端接口连接异常'
|
||||
} else if (message.includes('timeout')) {
|
||||
message = '系统接口请求超时'
|
||||
} else if (message.includes('Request failed with status code')) {
|
||||
message = '系统接口状态码异常'
|
||||
message = '系统接口' + message.substr(message.length - 3) + '异常'
|
||||
}
|
||||
ElMessage.error(message)
|
||||
return Promise.reject(error)
|
||||
|
@ -3,16 +3,16 @@ import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { intersection } from 'lodash-es'
|
||||
import { isArray } from '@/utils/is'
|
||||
import { useAuthStoreWithOut } from '@/store/modules/auth'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wsCache } = useCache()
|
||||
const authStore = useAuthStoreWithOut()
|
||||
const appStore = useAppStoreWithOut()
|
||||
|
||||
// 全部权限
|
||||
const all_permission = ['*.*.*']
|
||||
const hasPermission = (value: string | string[]): boolean => {
|
||||
const permissions = wsCache.get(authStore.getUserInfo).permissions as string[]
|
||||
const permissions = wsCache.get(appStore.getUserInfo).permissions as string[]
|
||||
if (!value) {
|
||||
throw new Error(t('permission.hasPermission'))
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ const TIME_AGO_MESSAGE_MAP: {
|
||||
},
|
||||
en: {
|
||||
justNow: '刚刚',
|
||||
invalid: 'Invalid Date',
|
||||
invalid: '无效时间',
|
||||
past: (n) => (n.match(/\d/) ? `${n} ago` : n),
|
||||
future: (n) => (n.match(/\d/) ? `in ${n}` : n),
|
||||
month: (n, past) =>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import router from './router'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useTitle } from '@/hooks/web/useTitle'
|
||||
@ -10,6 +11,7 @@ import { useAuthStoreWithOut } from '@/store/modules/auth'
|
||||
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
|
||||
const appStore = useAppStoreWithOut()
|
||||
const authStore = useAuthStoreWithOut()
|
||||
|
||||
const { wsCache } = useCache()
|
||||
@ -23,14 +25,14 @@ const whiteList = ['/login', '/docs/privacy', '/docs/agreement'] // 不重定向
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
if (wsCache.get(authStore.getUserInfo)) {
|
||||
if (wsCache.get(appStore.getUserInfo)) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else if (to.path === '/reset/password') {
|
||||
next()
|
||||
} else {
|
||||
if (!authStore.getIsUser) {
|
||||
await authStore.getUserInfoAction()
|
||||
await authStore.getUserInfo()
|
||||
}
|
||||
if (permissionStore.getIsAddRouters) {
|
||||
next()
|
||||
|
@ -27,6 +27,7 @@ interface AppState {
|
||||
pageLoading: boolean
|
||||
layout: LayoutType
|
||||
title: string
|
||||
userInfo: string
|
||||
isDark: boolean
|
||||
currentSize: ElementPlusSize
|
||||
sizeMap: ElementPlusSize[]
|
||||
@ -37,11 +38,14 @@ interface AppState {
|
||||
logoImage: string
|
||||
footerContent: string
|
||||
icpNumber: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: (): AppState => {
|
||||
return {
|
||||
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
|
||||
token: 'Token', // 存储Token字段
|
||||
sizeMap: ['default', 'large', 'small'],
|
||||
mobile: false, // 是否是移动端
|
||||
title: import.meta.env.VITE_APP_TITLE, // 标题
|
||||
@ -160,6 +164,12 @@ export const useAppStore = defineStore('app', {
|
||||
getTitle(): string {
|
||||
return this.title
|
||||
},
|
||||
getUserInfo(): string {
|
||||
return this.userInfo
|
||||
},
|
||||
getToken(): string {
|
||||
return this.token
|
||||
},
|
||||
getIsDark(): boolean {
|
||||
return this.isDark
|
||||
},
|
||||
|
@ -2,14 +2,12 @@ import { defineStore } from 'pinia'
|
||||
import { store } from '../index'
|
||||
import { UserLoginType } from '@/api/login/types'
|
||||
import { loginApi } from '@/api/login'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { getCurrentUserInfo } from '@/api/vadmin/auth/user'
|
||||
import { getCurrentAdminUserInfo } from '@/api/vadmin/auth/user'
|
||||
import { resetRouter } from '@/router'
|
||||
import { config } from '@/config/axios/config'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const { token } = config
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
export interface UserState {
|
||||
@ -24,7 +22,6 @@ export interface UserState {
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
userInfo: string
|
||||
user: UserState
|
||||
isUser: boolean
|
||||
}
|
||||
@ -32,7 +29,6 @@ export interface AuthState {
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: (): AuthState => {
|
||||
return {
|
||||
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
|
||||
user: {},
|
||||
isUser: false
|
||||
}
|
||||
@ -43,9 +39,6 @@ export const useAuthStore = defineStore('auth', {
|
||||
},
|
||||
getIsUser(): boolean {
|
||||
return this.isUser
|
||||
},
|
||||
getUserInfo(): string {
|
||||
return this.userInfo
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -53,9 +46,11 @@ export const useAuthStore = defineStore('auth', {
|
||||
formData.platform = '0'
|
||||
const res = await loginApi(formData)
|
||||
if (res) {
|
||||
wsCache.set(token, `${res.data.token_type} ${res.data.access_token}`)
|
||||
const appStore = useAppStore()
|
||||
wsCache.set(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
|
||||
// 存储用户信息
|
||||
await this.getUserInfoAction()
|
||||
const auth = useAuthStore()
|
||||
await auth.getUserInfo()
|
||||
}
|
||||
return res
|
||||
},
|
||||
@ -73,11 +68,13 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.user.name = data.name
|
||||
this.user.nickname = data.nickname
|
||||
this.user.telephone = data.telephone
|
||||
wsCache.set(this.userInfo, this.user)
|
||||
const appStore = useAppStore()
|
||||
wsCache.set(appStore.getUserInfo, this.user)
|
||||
},
|
||||
async getUserInfoAction() {
|
||||
const res = await getCurrentUserInfo()
|
||||
wsCache.set(this.userInfo, res.data)
|
||||
async getUserInfo() {
|
||||
const res = await getCurrentAdminUserInfo()
|
||||
const appStore = useAppStore()
|
||||
wsCache.set(appStore.getUserInfo, res.data)
|
||||
this.isUser = true
|
||||
this.user = res.data
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { ref, reactive } from 'vue'
|
||||
import { CountTo } from '@/components/CountTo'
|
||||
import { formatTime } from '@/utils'
|
||||
import { Highlight } from '@/components/Highlight'
|
||||
import { useAuthStoreWithOut } from '@/store/modules/auth'
|
||||
import {
|
||||
getCountApi,
|
||||
getProjectApi,
|
||||
@ -22,6 +21,7 @@ import type {
|
||||
Shortcuts
|
||||
} from '@/api/dashboard/workplace/types'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
import avatar from '@/assets/imgs/avatar.jpg'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
@ -97,9 +97,9 @@ getAllApi()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const authStore = useAuthStoreWithOut()
|
||||
const appStore = useAppStoreWithOut()
|
||||
|
||||
const user = wsCache.get(authStore.getUserInfo)
|
||||
const user = wsCache.get(appStore.getUserInfo)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -8,13 +8,6 @@ export const lineOptions: EChartsOption = {
|
||||
text: t('analysis.monthlySales'),
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
padding: [5, 10]
|
||||
},
|
||||
xAxis: {
|
||||
data: [
|
||||
t('analysis.january'),
|
||||
@ -42,6 +35,13 @@ export const lineOptions: EChartsOption = {
|
||||
top: 80,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
padding: [5, 10]
|
||||
},
|
||||
yAxis: {
|
||||
axisTick: {
|
||||
show: false
|
||||
|
@ -18,6 +18,7 @@ const props = defineProps({
|
||||
const rules = reactive({
|
||||
name: [required()],
|
||||
is_active: [required()],
|
||||
is_staff: [required()],
|
||||
role_ids: [required()],
|
||||
telephone: [required()]
|
||||
})
|
||||
|
@ -37,6 +37,11 @@ export const columns = reactive<TableColumn[]>([
|
||||
label: '是否可用',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
field: 'is_staff',
|
||||
label: '工作人员',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
field: 'last_login',
|
||||
label: '最近登录时间',
|
||||
@ -119,11 +124,35 @@ export const schema = reactive<FormSchema[]>([
|
||||
},
|
||||
value: '0'
|
||||
},
|
||||
{
|
||||
field: 'is_staff',
|
||||
label: '工作人员',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Radio',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '是',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: '否',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
},
|
||||
value: true
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '状态',
|
||||
colProps: {
|
||||
span: 12
|
||||
span: 24
|
||||
},
|
||||
component: 'Radio',
|
||||
componentProps: {
|
||||
@ -161,7 +190,8 @@ export const schema = reactive<FormSchema[]>([
|
||||
multiple: true,
|
||||
collapseTags: true
|
||||
},
|
||||
value: []
|
||||
value: [],
|
||||
ifshow: (values) => values.is_staff
|
||||
}
|
||||
])
|
||||
|
||||
@ -207,5 +237,25 @@ export const searchSchema = reactive<FormSchema[]>([
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'is_staff',
|
||||
label: '工作人员',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '214px'
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '是',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: '否',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
@ -305,6 +305,10 @@ const handleCommand = (command: string) => {
|
||||
<ElSwitch :value="row.is_active" disabled />
|
||||
</template>
|
||||
|
||||
<template #is_staff="{ row }">
|
||||
<ElSwitch :value="row.is_staff" disabled />
|
||||
</template>
|
||||
|
||||
<template #gender="{ row }">
|
||||
{{ selectDictLabel(genderOptions, row.gender) }}
|
||||
</template>
|
||||
|
154
kinit-admin/src/views/vadmin/help/issue/components/Write.vue
Normal file
154
kinit-admin/src/views/vadmin/help/issue/components/Write.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@/components/Form'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { ref, unref, reactive } from 'vue'
|
||||
import { schema } from './issue.data'
|
||||
import { ContentWrap } from '@/components/ContentWrap'
|
||||
import { ElMessage, ElButton } from 'element-plus'
|
||||
import { Editor, EditorExpose } from '@/components/Editor'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import {
|
||||
addIssueApi,
|
||||
getIssueApi,
|
||||
putIssueApi,
|
||||
getIssueCategoryOptionsApi
|
||||
} from '@/api/vadmin/help/issue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const { required } = useValidator()
|
||||
const { push, currentRoute } = useRouter()
|
||||
|
||||
const { register, methods, elFormRef } = useForm({
|
||||
schema: schema
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
title: [required()],
|
||||
content: [required()],
|
||||
category_id: [required()]
|
||||
})
|
||||
|
||||
const actionType = ref('')
|
||||
|
||||
const initData = async () => {
|
||||
const issueId = currentRoute.value.query.id
|
||||
if (issueId) {
|
||||
actionType.value = 'edit'
|
||||
const res = await getIssueApi(Number(issueId))
|
||||
if (res) {
|
||||
const { setValues } = methods
|
||||
setValues(res.data)
|
||||
} else {
|
||||
// 未获取到数据,跳转到404页面
|
||||
push('/404')
|
||||
}
|
||||
} else {
|
||||
actionType.value = 'add'
|
||||
}
|
||||
}
|
||||
|
||||
initData()
|
||||
|
||||
const getOptions = async () => {
|
||||
const { setSchema } = methods
|
||||
const res = await getIssueCategoryOptionsApi()
|
||||
setSchema([
|
||||
{
|
||||
field: 'category_id',
|
||||
path: 'componentProps.options',
|
||||
value: res.data
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
getOptions()
|
||||
|
||||
const editorRef = ref<typeof Editor & EditorExpose>()
|
||||
|
||||
const editorConfig = {
|
||||
customAlert: (s: string, t: string) => {
|
||||
switch (t) {
|
||||
case 'success':
|
||||
ElMessage.success(s)
|
||||
break
|
||||
case 'info':
|
||||
ElMessage.info(s)
|
||||
break
|
||||
case 'warning':
|
||||
ElMessage.warning(s)
|
||||
break
|
||||
case 'error':
|
||||
ElMessage.error(s)
|
||||
break
|
||||
default:
|
||||
ElMessage.info(s)
|
||||
break
|
||||
}
|
||||
},
|
||||
autoFocus: false,
|
||||
scroll: true,
|
||||
readOnly: false,
|
||||
uploadImgShowBase64: true,
|
||||
placeholder: '请输入内容...'
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const save = async () => {
|
||||
const formRef = unref(elFormRef)
|
||||
await formRef?.validate(async (isValid) => {
|
||||
if (isValid) {
|
||||
loading.value = true
|
||||
let data = await methods.getFormData()
|
||||
if (!data) {
|
||||
loading.value = false
|
||||
return ElMessage.error('未获取到数据')
|
||||
}
|
||||
const res = ref({})
|
||||
if (actionType.value === 'add') {
|
||||
res.value = await addIssueApi(data)
|
||||
} else if (actionType.value === 'edit') {
|
||||
res.value = await putIssueApi(data)
|
||||
}
|
||||
loading.value = false
|
||||
if (res.value) {
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
// 删除当前标签页,并跳转到列表页
|
||||
tagsViewStore.delView(unref(currentRoute))
|
||||
push('/help/issue')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
elFormRef,
|
||||
getFormData: methods.getFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Form class="issue-form" :rules="rules" @register="register">
|
||||
<template #content="form">
|
||||
<Editor
|
||||
v-model="form['content']"
|
||||
ref="editorRef"
|
||||
editorId="issueContent"
|
||||
:editorConfig="editorConfig"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #active>
|
||||
<ElButton type="primary" @click="save">立即保存</ElButton>
|
||||
</template>
|
||||
</Form>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.issue-form .el-form-item__content {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
147
kinit-admin/src/views/vadmin/help/issue/components/issue.data.ts
Normal file
147
kinit-admin/src/views/vadmin/help/issue/components/issue.data.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { TableColumn } from '@/types/table'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const columns = reactive<TableColumn[]>([
|
||||
{
|
||||
field: 'id',
|
||||
label: '编号',
|
||||
show: true,
|
||||
disabled: true,
|
||||
width: '120px',
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'category.name',
|
||||
label: '类别名称',
|
||||
show: true,
|
||||
disabled: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
label: '标题',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'view_number',
|
||||
label: '查看次数',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '是否可见',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'create_datetime',
|
||||
label: '创建时间',
|
||||
show: true,
|
||||
span: 24,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'user.name',
|
||||
label: '创建人',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
label: '操作',
|
||||
show: true,
|
||||
disabled: false,
|
||||
width: '100px',
|
||||
span: 24
|
||||
}
|
||||
])
|
||||
|
||||
export const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'title',
|
||||
label: '标题名称',
|
||||
component: 'Input',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
label: '解答内容',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'category_id',
|
||||
label: '问题类别',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'active',
|
||||
label: ' ',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
export const searchSchema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'title',
|
||||
label: '标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
style: {
|
||||
width: '214px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'category_id',
|
||||
label: '类别名称',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '是否可见',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '214px'
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '可见',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: '不可见',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
143
kinit-admin/src/views/vadmin/help/issue/index.vue
Normal file
143
kinit-admin/src/views/vadmin/help/issue/index.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { ContentWrap } from '@/components/ContentWrap'
|
||||
import { Table } from '@/components/Table'
|
||||
import {
|
||||
getIssueListApi,
|
||||
delIssueListApi,
|
||||
getIssueCategoryOptionsApi
|
||||
} from '@/api/vadmin/help/issue'
|
||||
import { useTable } from '@/hooks/web/useTable'
|
||||
import { columns, searchSchema } from './components/issue.data'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ElRow, ElCol, ElButton, ElSwitch } from 'element-plus'
|
||||
import { RightToolbar } from '@/components/RightToolbar'
|
||||
import { FormSetPropsType } from '@/types/form'
|
||||
import { Search } from '@/components/Search'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { register, elTableRef, tableObject, methods } = useTable({
|
||||
getListApi: getIssueListApi,
|
||||
delListApi: delIssueListApi,
|
||||
response: {
|
||||
data: 'data',
|
||||
count: 'count'
|
||||
}
|
||||
})
|
||||
|
||||
const { getList, setSearchParams } = methods
|
||||
|
||||
const tableSize = ref('default')
|
||||
|
||||
watch(tableSize, (val) => {
|
||||
tableSize.value = val
|
||||
})
|
||||
|
||||
const { currentRoute, push } = useRouter()
|
||||
const cacheTableHeadersKey = currentRoute.value.fullPath
|
||||
|
||||
watch(
|
||||
columns,
|
||||
async (val) => {
|
||||
wsCache.set(cacheTableHeadersKey, JSON.stringify(val))
|
||||
await nextTick()
|
||||
elTableRef.value?.doLayout()
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const loading = ref(false)
|
||||
const searchSetSchemaList = ref([] as FormSetPropsType[])
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await getIssueCategoryOptionsApi()
|
||||
searchSetSchemaList.value.push({
|
||||
field: 'category_id',
|
||||
path: 'componentProps.options',
|
||||
value: res.data
|
||||
})
|
||||
}
|
||||
|
||||
getOptions()
|
||||
|
||||
// 新增类别事件
|
||||
const auditAction = async () => {
|
||||
push('/help/issue/form')
|
||||
}
|
||||
|
||||
// 编辑事件
|
||||
const updateAction = async (row: any) => {
|
||||
push(`/help/issue/form?id=${row.id}`)
|
||||
}
|
||||
|
||||
// 删除事件
|
||||
const delData = async (row: any) => {
|
||||
tableObject.currentRow = row
|
||||
const { delListApi } = methods
|
||||
loading.value = true
|
||||
await delListApi([row.id], false).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Search
|
||||
:schema="searchSchema"
|
||||
:setSchemaList="searchSetSchemaList"
|
||||
@search="setSearchParams"
|
||||
@reset="setSearchParams"
|
||||
/>
|
||||
|
||||
<div class="mb-8px flex justify-between">
|
||||
<ElRow>
|
||||
<ElCol :span="1.5">
|
||||
<ElButton type="primary" @click="auditAction">新增问题</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<RightToolbar
|
||||
@get-list="getList"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:columns="columns"
|
||||
:cache-table-headers-key="cacheTableHeadersKey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
v-model:limit="tableObject.limit"
|
||||
v-model:page="tableObject.page"
|
||||
:columns="columns"
|
||||
:data="tableObject.tableData"
|
||||
:loading="tableObject.loading"
|
||||
:selection="false"
|
||||
:size="tableSize"
|
||||
:border="true"
|
||||
:pagination="{
|
||||
total: tableObject.count
|
||||
}"
|
||||
@register="register"
|
||||
>
|
||||
<template #is_active="{ row }">
|
||||
<ElSwitch :value="row.is_active" size="small" disabled />
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<ElButton type="primary" link size="small" @click="updateAction(row)">
|
||||
{{ t('exampleDemo.edit') }}
|
||||
</ElButton>
|
||||
<ElButton type="danger" link size="small" @click="delData(row)">
|
||||
{{ t('exampleDemo.del') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</template>
|
@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@/components/Form'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { PropType, reactive, watch } from 'vue'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import { schema } from './issueCategory.data'
|
||||
import { DictDetail } from '@/utils/dict'
|
||||
|
||||
const { required } = useValidator()
|
||||
|
||||
const props = defineProps({
|
||||
currentRow: {
|
||||
type: Object as PropType<Nullable<any>>,
|
||||
default: () => null
|
||||
},
|
||||
platformOptions: {
|
||||
type: Object as PropType<DictDetail[]>,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
name: [required()],
|
||||
platform: [required()],
|
||||
is_active: [required()]
|
||||
})
|
||||
|
||||
const { register, methods, elFormRef } = useForm({
|
||||
schema: schema
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.currentRow,
|
||||
(currentRow) => {
|
||||
if (!currentRow) return
|
||||
const { setValues } = methods
|
||||
setValues(currentRow)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.platformOptions,
|
||||
(platformOptions) => {
|
||||
if (!platformOptions) return
|
||||
const { setSchema } = methods
|
||||
setSchema([
|
||||
{
|
||||
field: 'platform',
|
||||
path: 'componentProps.options',
|
||||
value: platformOptions
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
elFormRef,
|
||||
getFormData: methods.getFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form :rules="rules" @register="register" />
|
||||
</template>
|
@ -0,0 +1,150 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { TableColumn } from '@/types/table'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const columns = reactive<TableColumn[]>([
|
||||
{
|
||||
field: 'id',
|
||||
label: '编号',
|
||||
show: true,
|
||||
disabled: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '类别名称',
|
||||
show: true,
|
||||
disabled: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
label: '展示平台',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '是否可见',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'create_datetime',
|
||||
label: '创建时间',
|
||||
show: true,
|
||||
span: 24,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'user.name',
|
||||
label: '创建人',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
label: '操作',
|
||||
show: true,
|
||||
disabled: false,
|
||||
span: 24
|
||||
}
|
||||
])
|
||||
|
||||
export const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'name',
|
||||
label: '类别名称',
|
||||
component: 'Input',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
label: '展示平台',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '是否可见',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Radio',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '可见',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: '不可见',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
},
|
||||
value: true
|
||||
}
|
||||
])
|
||||
|
||||
export const searchSchema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'name',
|
||||
label: '类别名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
style: {
|
||||
width: '214px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'platform',
|
||||
label: '展示平台',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '214px'
|
||||
},
|
||||
options: []
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'is_active',
|
||||
label: '是否可见',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '214px'
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '可见',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: '不可见',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
205
kinit-admin/src/views/vadmin/help/issueCategory/index.vue
Normal file
205
kinit-admin/src/views/vadmin/help/issueCategory/index.vue
Normal file
@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import { ContentWrap } from '@/components/ContentWrap'
|
||||
import { Table } from '@/components/Table'
|
||||
import {
|
||||
getIssueCategoryListApi,
|
||||
addIssueCategoryApi,
|
||||
getIssueCategoryApi,
|
||||
delIssueCategoryListApi,
|
||||
putIssueCategoryApi
|
||||
} from '@/api/vadmin/help/issue'
|
||||
import { useTable } from '@/hooks/web/useTable'
|
||||
import { columns, searchSchema } from './components/issueCategory.data'
|
||||
import { ref, watch, nextTick, unref } from 'vue'
|
||||
import { ElRow, ElCol, ElButton, ElSwitch } from 'element-plus'
|
||||
import { RightToolbar } from '@/components/RightToolbar'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { selectDictLabel, DictDetail } from '@/utils/dict'
|
||||
import { FormSetPropsType } from '@/types/form'
|
||||
import { Search } from '@/components/Search'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import Write from './components/Write.vue'
|
||||
import { Dialog } from '@/components/Dialog'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { register, elTableRef, tableObject, methods } = useTable({
|
||||
getListApi: getIssueCategoryListApi,
|
||||
delListApi: delIssueCategoryListApi,
|
||||
response: {
|
||||
data: 'data',
|
||||
count: 'count'
|
||||
}
|
||||
})
|
||||
|
||||
const { getList, setSearchParams } = methods
|
||||
|
||||
const tableSize = ref('default')
|
||||
|
||||
watch(tableSize, (val) => {
|
||||
tableSize.value = val
|
||||
})
|
||||
|
||||
const route = useRouter()
|
||||
const cacheTableHeadersKey = route.currentRoute.value.fullPath
|
||||
|
||||
watch(
|
||||
columns,
|
||||
async (val) => {
|
||||
wsCache.set(cacheTableHeadersKey, JSON.stringify(val))
|
||||
await nextTick()
|
||||
elTableRef.value?.doLayout()
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const loading = ref(false)
|
||||
const actionType = ref('')
|
||||
const platformOptions = ref<DictDetail[]>([])
|
||||
const searchSetSchemaList = ref([] as FormSetPropsType[])
|
||||
|
||||
const getOptions = async () => {
|
||||
const dictStore = useDictStore()
|
||||
const dictOptions = await dictStore.getDictObj(['sys_vadmin_platform'])
|
||||
platformOptions.value = dictOptions.sys_vadmin_platform
|
||||
searchSetSchemaList.value.push({
|
||||
field: 'platform',
|
||||
path: 'componentProps.options',
|
||||
value: dictOptions.sys_vadmin_platform
|
||||
})
|
||||
}
|
||||
|
||||
getOptions()
|
||||
|
||||
// 新增类别事件
|
||||
const addAction = async () => {
|
||||
dialogTitle.value = '新增类别'
|
||||
tableObject.currentRow = null
|
||||
dialogVisible.value = true
|
||||
actionType.value = 'add'
|
||||
}
|
||||
|
||||
// 编辑事件
|
||||
const updateAction = async (row: any) => {
|
||||
const res = await getIssueCategoryApi(row.id)
|
||||
dialogTitle.value = '编辑'
|
||||
tableObject.currentRow = res.data
|
||||
dialogVisible.value = true
|
||||
actionType.value = 'edit'
|
||||
}
|
||||
|
||||
// 删除事件
|
||||
const delData = async (row: any) => {
|
||||
tableObject.currentRow = row
|
||||
const { delListApi } = methods
|
||||
loading.value = true
|
||||
await delListApi([row.id], false).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const writeRef = ref<ComponentRef<typeof Write>>()
|
||||
|
||||
// 提交事件
|
||||
const save = async () => {
|
||||
const write = unref(writeRef)
|
||||
await write?.elFormRef?.validate(async (isValid) => {
|
||||
if (isValid) {
|
||||
loading.value = true
|
||||
let data = await write?.getFormData()
|
||||
const res = ref({})
|
||||
if (actionType.value === 'add') {
|
||||
res.value = await addIssueCategoryApi(data)
|
||||
} else if (actionType.value === 'edit') {
|
||||
res.value = await putIssueCategoryApi(data)
|
||||
}
|
||||
if (res.value) {
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Search
|
||||
:schema="searchSchema"
|
||||
:setSchemaList="searchSetSchemaList"
|
||||
@search="setSearchParams"
|
||||
@reset="setSearchParams"
|
||||
/>
|
||||
|
||||
<div class="mb-8px flex justify-between">
|
||||
<ElRow>
|
||||
<ElCol :span="1.5">
|
||||
<ElButton type="primary" @click="addAction">新增类别</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<RightToolbar
|
||||
@get-list="getList"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:columns="columns"
|
||||
:cache-table-headers-key="cacheTableHeadersKey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
v-model:limit="tableObject.limit"
|
||||
v-model:page="tableObject.page"
|
||||
:columns="columns"
|
||||
:data="tableObject.tableData"
|
||||
:loading="tableObject.loading"
|
||||
:selection="false"
|
||||
:size="tableSize"
|
||||
:border="true"
|
||||
:pagination="{
|
||||
total: tableObject.count
|
||||
}"
|
||||
@register="register"
|
||||
>
|
||||
<template #is_active="{ row }">
|
||||
<ElSwitch :value="row.is_active" size="small" disabled />
|
||||
</template>
|
||||
|
||||
<template #platform="{ row }">
|
||||
{{ selectDictLabel(platformOptions, row.platform) }}
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<ElButton type="primary" link size="small" @click="updateAction(row)">
|
||||
{{ t('exampleDemo.edit') }}
|
||||
</ElButton>
|
||||
<ElButton type="danger" link size="small" @click="delData(row)">
|
||||
{{ t('exampleDemo.del') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||
<Write
|
||||
ref="writeRef"
|
||||
:current-row="tableObject.currentRow"
|
||||
:platform-options="platformOptions"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<ElButton type="primary" :loading="loading" @click="save">
|
||||
{{ t('exampleDemo.save') }}
|
||||
</ElButton>
|
||||
<ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
|
||||
</template>
|
||||
</Dialog>
|
||||
</ContentWrap>
|
||||
</template>
|
@ -19,6 +19,7 @@ import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@/components/Search'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
const { push } = useRouter()
|
||||
@ -70,6 +71,13 @@ const toDetail = (row: any) => {
|
||||
push(`/system/dict/detail?dictType=${row.id}`)
|
||||
}
|
||||
|
||||
// 复制字典类型
|
||||
const toCopy = async (value: string) => {
|
||||
const { copy } = useClipboard()
|
||||
await copy(value)
|
||||
return ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
const writeRef = ref<ComponentRef<typeof Write>>()
|
||||
|
||||
const save = async () => {
|
||||
@ -164,6 +172,11 @@ watch(
|
||||
</template>
|
||||
|
||||
<template #dict_type="{ row }">
|
||||
<Icon
|
||||
icon="material-symbols:content-copy-rounded"
|
||||
class="cursor-pointer"
|
||||
@click="toCopy(row.dict_type)"
|
||||
/>
|
||||
<ElButton type="primary" link @click="toDetail(row)">
|
||||
{{ row.dict_type }}
|
||||
</ElButton>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getSystemSettingsConfigValueApi } from '@/api/vadmin/system/settings'
|
||||
import { getSystemAgreementApi } from '@/api/vadmin/system/settings'
|
||||
|
||||
const content = ref(null)
|
||||
|
||||
// 获取隐私协议内容
|
||||
const getSystemConfig = async () => {
|
||||
const res = await getSystemSettingsConfigValueApi({ config_key: 'web_agreement' })
|
||||
const res = await getSystemAgreementApi()
|
||||
if (res) {
|
||||
content.value = res.data
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getSystemSettingsConfigValueApi } from '@/api/vadmin/system/settings'
|
||||
import { getSystemPrivacyApi } from '@/api/vadmin/system/settings'
|
||||
|
||||
const content = ref(null)
|
||||
|
||||
// 获取隐私协议内容
|
||||
const getSystemConfig = async () => {
|
||||
const res = await getSystemSettingsConfigValueApi({ config_key: 'web_privacy' })
|
||||
const res = await getSystemPrivacyApi()
|
||||
if (res) {
|
||||
content.value = res.data
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export const columns = reactive<TableColumn[]>([
|
||||
{
|
||||
field: 'login_method',
|
||||
label: '认证方式',
|
||||
width: '120px',
|
||||
width: '150px',
|
||||
show: true,
|
||||
span: 24
|
||||
},
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'wx_server_app_id',
|
||||
label: 'AppID',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '500px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'wx_server_app_secret',
|
||||
label: 'AppSecret',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '500px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'wx_server_email',
|
||||
label: '官方邮件',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '500px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'wx_server_phone',
|
||||
label: '服务热线',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '500px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'wx_server_site',
|
||||
label: '官方邮箱',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '500px'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'active',
|
||||
label: '',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
}
|
||||
])
|
@ -5,6 +5,7 @@ import Basic from './basic.vue'
|
||||
import Baidu from './baidu.vue'
|
||||
import Privacy from './privacy.vue'
|
||||
import Agreement from './agreement.vue'
|
||||
import WXClient from './wxServer.vue'
|
||||
import { ContentWrap } from '@/components/ContentWrap'
|
||||
import { getSystemSettingsTabsApi } from '@/api/vadmin/system/settings'
|
||||
|
||||
@ -29,6 +30,7 @@ getList()
|
||||
<Baidu v-else-if="item.tab_name === 'web_baidu'" :tab-id="item.id" />
|
||||
<Privacy v-else-if="item.tab_name === 'web_privacy'" :tab-id="item.id" />
|
||||
<Agreement v-else-if="item.tab_name === 'web_agreement'" :tab-id="item.id" />
|
||||
<WXClient v-else-if="item.tab_name === 'wx_server'" :tab-id="item.id" />
|
||||
</ElTabPane>
|
||||
</template>
|
||||
</ElTabs>
|
||||
|
64
kinit-admin/src/views/vadmin/system/settings/wxServer.vue
Normal file
64
kinit-admin/src/views/vadmin/system/settings/wxServer.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@/components/Form'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { schema } from './components/wxServer.data'
|
||||
import { ElButton } from 'element-plus'
|
||||
import { getSystemSettingsApi, putSystemSettingsApi } from '@/api/vadmin/system/settings'
|
||||
import { ref, unref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
const props = defineProps({
|
||||
tabId: propTypes.number
|
||||
})
|
||||
|
||||
const { register, methods, elFormRef } = useForm({
|
||||
schema: schema
|
||||
})
|
||||
|
||||
const { setValues } = methods
|
||||
|
||||
let formData = ref({} as Recordable)
|
||||
|
||||
const getData = async () => {
|
||||
const res = await getSystemSettingsApi({ tab_id: props.tabId })
|
||||
if (res) {
|
||||
setValues(res.data)
|
||||
formData.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const save = async () => {
|
||||
const formRef = unref(elFormRef)
|
||||
await formRef?.validate(async (isValid) => {
|
||||
if (isValid) {
|
||||
loading.value = true
|
||||
let data = await methods.getFormData()
|
||||
if (!data) {
|
||||
loading.value = false
|
||||
return ElMessage.error('未获取到数据')
|
||||
}
|
||||
const res = await putSystemSettingsApi(data)
|
||||
if (res) {
|
||||
getData()
|
||||
return ElMessage.success('更新成功')
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form @register="register">
|
||||
<template #active>
|
||||
<ElButton type="primary" @click="save">立即提交</ElButton>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less"></style>
|
@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
|
||||
"""
|
||||
系统版本
|
||||
"""
|
||||
VERSION = "1.5.2"
|
||||
VERSION = "1.6.0"
|
||||
|
||||
"""安全警告: 不要在生产中打开调试运行!"""
|
||||
DEBUG = True
|
||||
@ -21,6 +21,7 @@ DEMO = not DEBUG
|
||||
"""演示功能白名单"""
|
||||
DEMO_WHITE_LIST_PATH = [
|
||||
"/auth/login/",
|
||||
"/auth/wx/login/",
|
||||
"/vadmin/system/dict/types/details/",
|
||||
"/vadmin/auth/user/export/query/list/to/excel/"
|
||||
]
|
||||
|
@ -15,4 +15,5 @@ urlpatterns = [
|
||||
{"ApiRouter": vadmin_record_app, "prefix": "/vadmin/record", "tags": ["记录管理"]},
|
||||
{"ApiRouter": vadmin_workplace_app, "prefix": "/vadmin/workplace", "tags": ["工作区管理"]},
|
||||
{"ApiRouter": vadmin_analysis_app, "prefix": "/vadmin/analysis", "tags": ["数据分析管理"]},
|
||||
]
|
||||
{"ApiRouter": vadmin_help_app, "prefix": "/vadmin/help", "tags": ["帮助中心管理"]},
|
||||
]
|
||||
|
@ -13,3 +13,5 @@ from apps.vadmin.system.views import app as vadmin_system_app
|
||||
from apps.vadmin.record.views import app as vadmin_record_app
|
||||
from apps.vadmin.workplace.views import app as vadmin_workplace_app
|
||||
from apps.vadmin.analysis.views import app as vadmin_analysis_app
|
||||
from apps.vadmin.help.views import app as vadmin_help_app
|
||||
|
||||
|
@ -7,8 +7,9 @@
|
||||
# @desc : 简要说明
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth
|
||||
from utils.response import SuccessResponse
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
@ -17,7 +18,7 @@ app = APIRouter()
|
||||
# 图表数据
|
||||
###########################################################
|
||||
@app.get("/banners/", summary="轮播图")
|
||||
async def get_banners(auth: Auth = Depends(login_auth)):
|
||||
async def get_banners(auth: Auth = Depends(AllUserAuth())):
|
||||
data = [
|
||||
{
|
||||
"id": 1, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-14/1.jpg"
|
||||
|
@ -9,6 +9,8 @@
|
||||
from typing import List, Any
|
||||
from aioredis import Redis
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from core.exception import CustomException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import select
|
||||
@ -27,6 +29,7 @@ from utils.excel.excel_manage import ExcelManage
|
||||
from apps.vadmin.system import crud as vadminSystemCRUD
|
||||
import copy
|
||||
from utils import status
|
||||
from utils.wx.oauth import WXOAuth
|
||||
|
||||
|
||||
class UserDal(DalBase):
|
||||
@ -58,8 +61,9 @@ class UserDal(DalBase):
|
||||
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
|
||||
data.password = self.model.get_password_hash(password)
|
||||
obj = self.model(**data.dict(exclude={'role_ids'}))
|
||||
for data_id in data.role_ids:
|
||||
obj.roles.append(await RoleDal(db=self.db).get_data(data_id=data_id))
|
||||
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
|
||||
for role in roles:
|
||||
obj.roles.append(role)
|
||||
await self.flush(obj)
|
||||
if v_options:
|
||||
obj = await self.get_data(obj.id, v_options=v_options)
|
||||
@ -69,6 +73,37 @@ class UserDal(DalBase):
|
||||
return v_schema.from_orm(obj).dict()
|
||||
return self.out_dict(obj)
|
||||
|
||||
async def put_data(
|
||||
self,
|
||||
data_id: int,
|
||||
data: schemas.UserUpdate,
|
||||
v_options: list = None,
|
||||
v_return_obj: bool = False,
|
||||
v_schema: Any = None
|
||||
):
|
||||
"""
|
||||
更新用户信息
|
||||
"""
|
||||
obj = await self.get_data(data_id, v_options=[joinedload(self.model.roles)])
|
||||
data_dict = jsonable_encoder(data)
|
||||
for key, value in data_dict.items():
|
||||
if key == "role_ids":
|
||||
if obj.roles:
|
||||
obj.roles.clear()
|
||||
if value:
|
||||
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
|
||||
for role in roles:
|
||||
obj.roles.append(role)
|
||||
continue
|
||||
setattr(obj, key, value)
|
||||
await self.db.flush()
|
||||
await self.db.refresh(obj)
|
||||
if v_return_obj:
|
||||
return obj
|
||||
if v_schema:
|
||||
return v_schema.from_orm(obj).dict()
|
||||
return self.out_dict(obj)
|
||||
|
||||
async def reset_current_password(self, user: models.VadminUser, data: schemas.ResetPwd):
|
||||
"""
|
||||
重置密码
|
||||
@ -83,9 +118,9 @@ class UserDal(DalBase):
|
||||
await self.flush(user)
|
||||
return True
|
||||
|
||||
async def update_current_info(self, user: models.VadminUser, data: schemas.UserUpdate):
|
||||
async def update_current_info(self, user: models.VadminUser, data: schemas.UserUpdateBaseInfo):
|
||||
"""
|
||||
更新当前用户信息
|
||||
更新当前用户基本信息
|
||||
"""
|
||||
if data.telephone != user.telephone:
|
||||
unique = await self.get_data(telephone=data.telephone, v_return_none=True)
|
||||
@ -225,6 +260,34 @@ class UserDal(DalBase):
|
||||
await self.flush(user)
|
||||
return result
|
||||
|
||||
async def update_wx_server_openid(self, code: str, user: models.VadminUser, redis: Redis):
|
||||
"""
|
||||
更新用户服务端微信平台openid
|
||||
"""
|
||||
wx = WXOAuth(redis, 0)
|
||||
openid = await wx.parsing_openid(code)
|
||||
if not openid:
|
||||
return False
|
||||
user.is_wx_server_openid = True
|
||||
user.wx_server_openid = openid
|
||||
await self.flush(user)
|
||||
return True
|
||||
|
||||
async def delete_datas(self, ids: List[int], v_soft: bool = False, **kwargs):
|
||||
"""
|
||||
删除多个用户,软删除
|
||||
删除后清空所关联的角色
|
||||
:param ids: 数据集
|
||||
:param v_soft: 是否执行软删除
|
||||
:param kwargs: 其他更新字段
|
||||
"""
|
||||
options = [joinedload(self.model.roles)]
|
||||
objs = await self.get_datas(limit=0, id=("in", ids), v_options=options, v_return_objs=True)
|
||||
for obj in objs:
|
||||
if obj.roles:
|
||||
obj.roles.clear()
|
||||
return await super(UserDal, self).delete_datas(ids, v_soft, **kwargs)
|
||||
|
||||
|
||||
class RoleDal(DalBase):
|
||||
|
||||
@ -240,8 +303,10 @@ class RoleDal(DalBase):
|
||||
):
|
||||
"""创建数据"""
|
||||
obj = self.model(**data.dict(exclude={'menu_ids'}))
|
||||
for data_id in data.menu_ids:
|
||||
obj.menus.append(await MenuDal(db=self.db).get_data(data_id))
|
||||
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", data.menu_ids), v_return_objs=True)
|
||||
if data.menu_ids:
|
||||
for menu in menus:
|
||||
obj.menus.append(menu)
|
||||
await self.flush(obj)
|
||||
if v_options:
|
||||
obj = await self.get_data(obj.id, v_options=v_options)
|
||||
@ -260,14 +325,16 @@ class RoleDal(DalBase):
|
||||
v_schema: Any = None
|
||||
):
|
||||
"""更新单个数据"""
|
||||
obj = await self.get_data(data_id, v_options=[self.model.menus])
|
||||
obj = await self.get_data(data_id, v_options=[joinedload(self.model.menus)])
|
||||
obj_dict = jsonable_encoder(data)
|
||||
for key, value in obj_dict.items():
|
||||
if key == "menu_ids":
|
||||
if obj.menus:
|
||||
obj.menus.clear()
|
||||
for data_id in value:
|
||||
obj.menus.append(await MenuDal(db=self.db).get_data(data_id=data_id))
|
||||
if value:
|
||||
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
|
||||
for menu in menus:
|
||||
obj.menus.append(menu)
|
||||
continue
|
||||
setattr(obj, key, value)
|
||||
await self.db.flush()
|
||||
@ -279,7 +346,7 @@ class RoleDal(DalBase):
|
||||
return self.out_dict(obj)
|
||||
|
||||
async def get_role_menu_tree(self, role_id: int):
|
||||
role = await self.get_data(role_id, v_options=[self.model.menus])
|
||||
role = await self.get_data(role_id, v_options=[joinedload(self.model.menus)])
|
||||
return [i.id for i in role.menus]
|
||||
|
||||
async def get_select_datas(self):
|
||||
@ -288,6 +355,19 @@ class RoleDal(DalBase):
|
||||
queryset = await self.db.execute(sql)
|
||||
return [schemas.RoleSelectOut.from_orm(i).dict() for i in queryset.scalars().all()]
|
||||
|
||||
async def delete_datas(self, ids: List[int], v_soft: bool = False, **kwargs):
|
||||
"""
|
||||
删除多个角色,硬删除
|
||||
如果存在用户关联则无法删除
|
||||
:param ids: 数据集
|
||||
:param v_soft: 是否执行软删除
|
||||
:param kwargs: 其他更新字段
|
||||
"""
|
||||
objs = await self.get_datas(limit=0, id=("in", ids), user_total_number=(">", 0), v_return_objs=True)
|
||||
if objs:
|
||||
raise CustomException("无法删除存在用户关联的角色", code=400)
|
||||
return await super(RoleDal, self).delete_datas(ids, v_soft, **kwargs)
|
||||
|
||||
|
||||
class MenuDal(DalBase):
|
||||
|
||||
@ -301,9 +381,9 @@ class MenuDal(DalBase):
|
||||
3:获取菜单树列表,角色添加菜单权限时使用
|
||||
"""
|
||||
if mode == 3:
|
||||
sql = select(self.model).where(self.model.disabled == 0, self.model.delete_datetime.is_(None))
|
||||
sql = select(self.model).where(self.model.disabled == 0, self.model.is_delete == False)
|
||||
else:
|
||||
sql = select(self.model).where(self.model.delete_datetime.is_(None))
|
||||
sql = select(self.model).where(self.model.is_delete == False)
|
||||
queryset = await self.db.execute(sql)
|
||||
datas = queryset.scalars().all()
|
||||
roots = filter(lambda i: not i.parent_id, datas)
|
||||
@ -329,14 +409,15 @@ class MenuDal(DalBase):
|
||||
"""
|
||||
if any([i.is_admin for i in user.roles]):
|
||||
sql = select(self.model)\
|
||||
.where(self.model.disabled == 0, self.model.menu_type != "2", self.model.delete_datetime.is_(None))
|
||||
.where(self.model.disabled == 0, self.model.menu_type != "2", self.model.is_delete == False)
|
||||
queryset = await self.db.execute(sql)
|
||||
datas = queryset.scalars().all()
|
||||
else:
|
||||
options = [joinedload(models.VadminUser.roles), joinedload("roles.menus")]
|
||||
user = await UserDal(self.db).get_data(user.id, v_options=options)
|
||||
datas = set()
|
||||
for role in user.roles:
|
||||
role_obj = await RoleDal(self.db).get_data(role.id, v_options=[models.VadminRole.menus])
|
||||
for menu in role_obj.menus:
|
||||
for menu in role.menus:
|
||||
# 该路由没有被禁用,并且菜单不是按钮
|
||||
if not menu.disabled and menu.menu_type != "2":
|
||||
datas.add(menu)
|
||||
@ -406,3 +487,18 @@ class MenuDal(DalBase):
|
||||
item[children] = sorted(item[children], key=lambda menu: menu[order])
|
||||
return result
|
||||
|
||||
async def delete_datas(self, ids: List[int], v_soft: bool = False, **kwargs):
|
||||
"""
|
||||
删除多个菜单
|
||||
如果存在角色关联则无法删除
|
||||
:param ids: 数据集
|
||||
:param v_soft: 是否执行软删除
|
||||
:param kwargs: 其他更新字段
|
||||
"""
|
||||
options = [joinedload(self.model.roles)]
|
||||
objs = await self.get_datas(limit=0, id=("in", ids), v_return_objs=True, v_options=options)
|
||||
for obj in objs:
|
||||
if obj.roles:
|
||||
raise CustomException("无法删除存在角色关联的菜单", code=400)
|
||||
return await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
|
||||
|
||||
|
@ -7,8 +7,10 @@
|
||||
# @desc : 角色模型
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import aggregated
|
||||
from .user import VadminUser
|
||||
from db.db_base import BaseModel
|
||||
from sqlalchemy import Column, String, Boolean, Integer
|
||||
from sqlalchemy import Column, String, Boolean, Integer, func
|
||||
from .m2m import vadmin_user_roles, vadmin_role_menus
|
||||
|
||||
|
||||
@ -25,3 +27,7 @@ class VadminRole(BaseModel):
|
||||
|
||||
users = relationship("VadminUser", back_populates='roles', secondary=vadmin_user_roles)
|
||||
menus = relationship("VadminMenu", back_populates='roles', secondary=vadmin_role_menus)
|
||||
|
||||
@aggregated('users', Column(Integer, default=0, comment="用户总数"))
|
||||
def user_total_number(self):
|
||||
return func.count(VadminUser.id)
|
||||
|
@ -22,16 +22,18 @@ class VadminUser(BaseModel):
|
||||
__table_args__ = ({'comment': '用户表'})
|
||||
|
||||
avatar = Column(String(500), nullable=True, comment='头像')
|
||||
telephone = Column(String(11), nullable=False, index=True, comment="手机号", unique=True)
|
||||
telephone = Column(String(11), nullable=False, index=True, comment="手机号", unique=False)
|
||||
name = Column(String(50), index=True, nullable=False, comment="姓名")
|
||||
nickname = Column(String(50), nullable=True, comment="昵称")
|
||||
password = Column(String(255), nullable=True, comment="密码")
|
||||
gender = Column(String(8), nullable=True, comment="性别")
|
||||
is_active = Column(Boolean, default=True, comment="是否可用")
|
||||
is_cancel = Column(Boolean, default=False, comment="是否注销")
|
||||
is_reset_password = Column(Boolean, default=False, comment="是否已经重置密码,没有重置的,登陆系统后必须重置密码")
|
||||
last_ip = Column(String(50), nullable=True, comment="最后一次登录IP")
|
||||
last_login = Column(DateTime, nullable=True, comment="最近一次登录时间")
|
||||
is_staff = Column(Boolean, default=False, comment="是否为工作人员")
|
||||
wx_server_openid = Column(String(255), comment="服务端微信平台openid")
|
||||
is_wx_server_openid = Column(Boolean, default=False, comment="是否已有服务端微信平台openid")
|
||||
|
||||
roles = relationship("VadminRole", back_populates='users', secondary=vadmin_user_roles)
|
||||
|
||||
@ -48,9 +50,9 @@ class VadminUser(BaseModel):
|
||||
async def update_login_info(self, db: AsyncSession, last_ip: str):
|
||||
"""
|
||||
更新当前登录信息
|
||||
@param db: 数据库
|
||||
@param last_ip: 最近一次登录 IP
|
||||
@return:
|
||||
:param db: 数据库
|
||||
:param last_ip: 最近一次登录 IP
|
||||
:return:
|
||||
"""
|
||||
self.last_ip = last_ip
|
||||
self.last_login = datetime.datetime.now()
|
||||
@ -62,6 +64,6 @@ class VadminUser(BaseModel):
|
||||
|
||||
以最高权限为准
|
||||
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
return any([i.is_admin for i in self.roles])
|
||||
|
@ -23,11 +23,13 @@ class UserParams(QueryParams):
|
||||
name: str = None,
|
||||
telephone: str = None,
|
||||
is_active: bool | str = None,
|
||||
is_staff: bool | str = None,
|
||||
params: Paging = Depends()
|
||||
):
|
||||
super().__init__(params)
|
||||
self.name = ("like", name)
|
||||
self.telephone = ("like", telephone)
|
||||
self.is_active = is_active
|
||||
self.is_staff = is_staff
|
||||
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd
|
||||
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo
|
||||
from .role import Role, RoleOut, RoleIn, RoleSelectOut, RoleSimpleOut
|
||||
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, TreeListOut
|
||||
|
@ -16,20 +16,48 @@ from .role import RoleSimpleOut
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
name: Optional[str] = None
|
||||
telephone: Telephone
|
||||
nickname: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_cancel: Optional[bool] = False
|
||||
is_staff: Optional[bool] = False
|
||||
gender: Optional[str] = "0"
|
||||
is_wx_server_openid: Optional[bool] = False
|
||||
|
||||
|
||||
class UserIn(User):
|
||||
"""
|
||||
创建用户
|
||||
"""
|
||||
role_ids: Optional[List[int]] = []
|
||||
password: Optional[str] = ""
|
||||
|
||||
|
||||
class UserUpdateBaseInfo(BaseModel):
|
||||
"""
|
||||
更新用户基本信息
|
||||
"""
|
||||
name: str
|
||||
telephone: Telephone
|
||||
nickname: Optional[str] = None
|
||||
gender: Optional[str] = "0"
|
||||
|
||||
|
||||
class UserUpdate(User):
|
||||
"""
|
||||
更新用户详细信息
|
||||
"""
|
||||
name: Optional[str] = None
|
||||
telephone: Telephone
|
||||
nickname: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_staff: Optional[bool] = False
|
||||
gender: Optional[str] = "0"
|
||||
role_ids: Optional[List[int]] = []
|
||||
|
||||
|
||||
class UserSimpleOut(User):
|
||||
id: int
|
||||
update_datetime: DatetimeStr
|
||||
@ -50,13 +78,6 @@ class UserOut(UserSimpleOut):
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
name: str
|
||||
telephone: Telephone
|
||||
nickname: Optional[str] = None
|
||||
gender: Optional[str] = "0"
|
||||
|
||||
|
||||
class ResetPwd(BaseModel):
|
||||
password: str
|
||||
password_two: str
|
||||
|
@ -5,46 +5,89 @@
|
||||
# @IDE : PyCharm
|
||||
# @desc : 获取认证后的信息工具
|
||||
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from apps.vadmin.auth import crud, models
|
||||
from .validation import AuthValidation, Auth
|
||||
from sqlalchemy.orm import joinedload
|
||||
from apps.vadmin.auth.crud import UserDal
|
||||
from apps.vadmin.auth.models import VadminUser
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
from .validation import AuthValidation
|
||||
from fastapi import Request, Depends
|
||||
from application import settings
|
||||
from core.database import db_getter
|
||||
from .validation.auth import Auth
|
||||
|
||||
|
||||
async def get_user_permissions(user):
|
||||
def get_user_permissions(user: VadminUser) -> set:
|
||||
"""
|
||||
获取跟进系统用户所有权限列表
|
||||
获取员工用户所有权限列表
|
||||
"""
|
||||
if any([role.is_admin for role in user.roles]):
|
||||
return ['*.*.*']
|
||||
return {'*.*.*'}
|
||||
permissions = set()
|
||||
for role_obj in user.roles:
|
||||
for menu in role_obj.menus:
|
||||
if menu.perms and not menu.disabled:
|
||||
permissions.add(menu.perms)
|
||||
return list(permissions)
|
||||
return permissions
|
||||
|
||||
|
||||
@AuthValidation
|
||||
async def login_auth(telephone: str, db: AsyncSession):
|
||||
class AllUserAuth(AuthValidation):
|
||||
|
||||
"""
|
||||
更新 login_auth 以接收 JWT 令牌。
|
||||
|
||||
解码接收到的令牌,对其进行校验,然后返回当前用户。
|
||||
|
||||
如果令牌无效,立即返回一个 HTTP 错误。
|
||||
支持所有用户认证
|
||||
获取用户基本信息
|
||||
"""
|
||||
return await crud.UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
telephone = self.validate_token(token, db)
|
||||
if isinstance(telephone, Auth):
|
||||
return telephone
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
return await self.validate_user(request, user, db)
|
||||
|
||||
|
||||
@AuthValidation
|
||||
async def full_admin(telephone: str, db: AsyncSession):
|
||||
class FullAdminAuth(AuthValidation):
|
||||
|
||||
"""
|
||||
更新 full_user 以接收 JWT 令牌。
|
||||
|
||||
解码接收到的令牌,对其进行校验,然后返回当前用户。
|
||||
|
||||
如果令牌无效,立即返回一个 HTTP 错误。
|
||||
只支持员工用户认证
|
||||
获取员工用户完整信息
|
||||
如果有权限,那么会验证该用户是否包括权限列表中的其中一个权限
|
||||
"""
|
||||
options = [models.VadminUser.roles, "roles.menus"]
|
||||
return await crud.UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options)
|
||||
|
||||
def __init__(self, permissions: Optional[List[str]] = None):
|
||||
if permissions:
|
||||
self.permissions = set(permissions)
|
||||
else:
|
||||
self.permissions = None
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> Auth:
|
||||
"""
|
||||
每次调用依赖此类的接口会执行该方法
|
||||
"""
|
||||
telephone = self.validate_token(token, db)
|
||||
if isinstance(telephone, Auth):
|
||||
return telephone
|
||||
options = [joinedload(VadminUser.roles), joinedload("roles.menus")]
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options, is_staff=True)
|
||||
result = await self.validate_user(request, user, db)
|
||||
permissions = get_user_permissions(user)
|
||||
if permissions != {'*.*.*'} and self.permissions:
|
||||
if not (self.permissions & permissions):
|
||||
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
|
||||
return result
|
||||
|
||||
|
@ -25,16 +25,21 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.database import db_getter
|
||||
from utils.response import SuccessResponse, ErrorResponse
|
||||
from application import settings
|
||||
from utils.tools import generate_string
|
||||
from .login_manage import LoginManage
|
||||
from .validation import LoginForm
|
||||
from .validation import LoginForm, WXLoginForm
|
||||
from apps.vadmin.record.models import VadminLoginRecord
|
||||
from apps.vadmin.auth.crud import MenuDal
|
||||
from .current import full_admin, Auth
|
||||
from apps.vadmin.auth.crud import MenuDal, UserDal
|
||||
from apps.vadmin.auth.schemas import UserIn
|
||||
from .current import FullAdminAuth
|
||||
from .validation.auth import Auth
|
||||
from utils.wx.oauth import WXOAuth
|
||||
from core.data_types import Telephone
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
|
||||
@app.post("/login/", summary="登录")
|
||||
@app.post("/login/", summary="手机号密码登录")
|
||||
async def login_for_access_token(
|
||||
request: Request,
|
||||
data: LoginForm,
|
||||
@ -49,22 +54,70 @@ async def login_for_access_token(
|
||||
return ErrorResponse(msg="请使用正确的登录方式")
|
||||
if not result.status:
|
||||
resp = {"message": result.msg}
|
||||
await VadminLoginRecord\
|
||||
.create_login_record(db, data, result.status, request, resp)
|
||||
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
|
||||
return ErrorResponse(msg=result.msg)
|
||||
|
||||
user = result.user
|
||||
|
||||
if data.platform in ["0", "1"] and not user.is_staff:
|
||||
msg = "此手机号无登录权限"
|
||||
await VadminLoginRecord.create_login_record(db, data, result.status, request, {"message": msg})
|
||||
return ErrorResponse(msg=msg)
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
|
||||
resp = {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"is_reset_password": user.is_reset_password
|
||||
"is_reset_password": user.is_reset_password,
|
||||
"is_wx_server_openid": user.is_wx_server_openid
|
||||
}
|
||||
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
|
||||
return SuccessResponse(resp)
|
||||
|
||||
|
||||
@app.post("/wx/login/", summary="微信服务端一键登录")
|
||||
async def wx_login_for_access_token(request: Request, data: WXLoginForm, db: AsyncSession = Depends(db_getter)):
|
||||
if data.platform not in ["0", "1"]:
|
||||
msg = "错误平台"
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
|
||||
return ErrorResponse(msg=msg)
|
||||
wx = WXOAuth(request.app.state.redis, 0)
|
||||
telephone = await wx.parsing_phone_number(data.code)
|
||||
if not telephone:
|
||||
msg = "无效Code"
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
|
||||
return ErrorResponse(msg=msg)
|
||||
data.telephone = telephone
|
||||
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
|
||||
msg = None
|
||||
if not user:
|
||||
# 手机号不存在,创建新用户
|
||||
# model = UserIn(name=generate_string(), telephone=Telephone(telephone))
|
||||
# user = await UserDal(db).create_data(model, v_return_obj=True)
|
||||
msg = "手机号不存在!"
|
||||
elif not user.is_active:
|
||||
msg = "此手机号已被冻结!"
|
||||
elif data.platform in ["0", "1"] and not user.is_staff:
|
||||
msg = "此手机号无登录权限"
|
||||
if msg:
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
|
||||
return ErrorResponse(msg=msg)
|
||||
# 更新登录时间
|
||||
await user.update_login_info(db, request.client.host)
|
||||
|
||||
# 登录成功创建 token
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
|
||||
resp = {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"is_reset_password": user.is_reset_password,
|
||||
"is_wx_server_openid": user.is_wx_server_openid
|
||||
}
|
||||
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
|
||||
return SuccessResponse(resp)
|
||||
|
||||
|
||||
@app.get("/getMenuList/", summary="获取当前用户菜单树")
|
||||
async def get_menu_list(auth: Auth = Depends(full_admin)):
|
||||
async def get_menu_list(auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse(await MenuDal(auth.db).get_routers(auth.user))
|
||||
|
@ -7,4 +7,4 @@
|
||||
# @desc : 简要说明
|
||||
|
||||
from .auth import Auth, AuthValidation
|
||||
from .login import LoginValidation, LoginForm, LoginResult
|
||||
from .login import LoginValidation, LoginForm, LoginResult, WXLoginForm
|
||||
|
@ -5,13 +5,12 @@
|
||||
# @IDE : PyCharm
|
||||
# @desc : 用户凭证验证装饰器
|
||||
|
||||
from fastapi import Request, Depends
|
||||
from fastapi import Request
|
||||
from jose import jwt, JWTError
|
||||
from pydantic import BaseModel
|
||||
from application import settings
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from apps.vadmin.auth import models
|
||||
from core.database import db_getter
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
|
||||
@ -27,18 +26,14 @@ class Auth(BaseModel):
|
||||
class AuthValidation:
|
||||
|
||||
"""
|
||||
验证提交 Token 与用户是否有效
|
||||
用于用户每次调用接口时,验证用户提交的token是否正确,并从token中获取用户信息
|
||||
"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
request: Request,
|
||||
token: str = Depends(settings.oauth2_scheme),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
@classmethod
|
||||
def validate_token(cls, token: str, db: AsyncSession) -> str | Auth:
|
||||
"""
|
||||
验证用户 token
|
||||
"""
|
||||
if not settings.OAUTH_ENABLE:
|
||||
return Auth(db=db)
|
||||
if not token:
|
||||
@ -50,13 +45,17 @@ class AuthValidation:
|
||||
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
|
||||
except JWTError:
|
||||
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
|
||||
user = await self.func(telephone, db)
|
||||
return telephone
|
||||
|
||||
@classmethod
|
||||
async def validate_user(cls, request: Request, user: models.VadminUser, db: AsyncSession) -> Auth:
|
||||
"""
|
||||
验证用户信息
|
||||
"""
|
||||
if user is None:
|
||||
raise CustomException(msg="认证已过期,请您重新登陆", code=status.HTTP_401_UNAUTHORIZED)
|
||||
elif not user.is_active:
|
||||
raise CustomException(msg="用户已被冻结!", code=status.HTTP_403_FORBIDDEN)
|
||||
elif user.is_cancel:
|
||||
raise CustomException(msg="用户已被注销!", code=status.HTTP_403_FORBIDDEN)
|
||||
request.scope["telephone"] = user.telephone
|
||||
try:
|
||||
request.scope["body"] = await request.body()
|
||||
|
@ -18,13 +18,20 @@ from typing import Optional
|
||||
class LoginForm(BaseModel):
|
||||
telephone: str
|
||||
password: str
|
||||
method: str = '0' # 认证方式,0:密码登录,1:短信登录
|
||||
method: str = '0' # 认证方式,0:密码登录,1:短信登录,2:微信一键登录
|
||||
platform: str = '0' # 登录平台,0:PC端管理系统,1:移动端管理系统
|
||||
|
||||
# validators
|
||||
_normalize_telephone = validator('telephone', allow_reuse=True)(vali_telephone)
|
||||
|
||||
|
||||
class WXLoginForm(BaseModel):
|
||||
telephone: Optional[str] = None
|
||||
code: str
|
||||
method: str = '2' # 认证方式,0:密码登录,1:短信登录,2:微信一键登录
|
||||
platform: str = '1' # 登录平台,0:PC端管理系统,1:移动端管理系统
|
||||
|
||||
|
||||
class LoginResult(BaseModel):
|
||||
status: Optional[bool] = False
|
||||
user: Optional[schemas.UserOut] = None
|
||||
@ -45,8 +52,10 @@ class LoginValidation:
|
||||
|
||||
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
|
||||
self.result = LoginResult()
|
||||
options = [models.VadminUser.roles, "roles.menus"]
|
||||
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True, v_options=options)
|
||||
if data.platform not in ["0", "1"]:
|
||||
self.result.msg = "错误平台"
|
||||
return self.result
|
||||
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
|
||||
if not user:
|
||||
self.result.msg = "该手机号不存在!"
|
||||
return self.result
|
||||
@ -57,11 +66,9 @@ class LoginValidation:
|
||||
self.result.msg = result.msg
|
||||
elif not user.is_active:
|
||||
self.result.msg = "此手机号已被冻结!"
|
||||
elif user.is_cancel:
|
||||
self.result.msg = "此手机号已被注销!"
|
||||
elif user:
|
||||
self.result.msg = "OK"
|
||||
self.result.status = True
|
||||
self.result.user = schemas.UserOut.from_orm(user)
|
||||
self.result.user = schemas.UserSimpleOut.from_orm(user)
|
||||
await user.update_login_info(db, request.client.host)
|
||||
return self.result
|
||||
|
@ -7,10 +7,12 @@
|
||||
# @desc : 简要说明
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, UploadFile, Request
|
||||
from sqlalchemy.orm import joinedload
|
||||
from utils.response import SuccessResponse, ErrorResponse
|
||||
from . import schemas, crud, models
|
||||
from core.dependencies import IdList
|
||||
from apps.vadmin.auth.utils.current import login_auth, Auth, get_user_permissions, full_admin
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth, get_user_permissions, FullAdminAuth
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
from .params import UserParams, RoleParams
|
||||
|
||||
app = APIRouter()
|
||||
@ -20,59 +22,72 @@ app = APIRouter()
|
||||
# 用户管理
|
||||
###########################################################
|
||||
@app.get("/users/", summary="获取用户列表")
|
||||
async def get_users(params: UserParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
datas = await crud.UserDal(auth.db).get_datas(**params.dict())
|
||||
async def get_users(
|
||||
params: UserParams = Depends(),
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.list"]))
|
||||
):
|
||||
model = models.VadminUser
|
||||
options = [joinedload(model.roles)]
|
||||
schema = schemas.UserOut
|
||||
datas = await crud.UserDal(auth.db).get_datas(**params.dict(), v_options=options, v_schema=schema)
|
||||
count = await crud.UserDal(auth.db).get_count(**params.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.post("/users/", summary="创建用户")
|
||||
async def create_user(data: schemas.UserIn, auth: Auth = Depends(login_auth)):
|
||||
async def create_user(data: schemas.UserIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create"]))):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).create_data(data=data))
|
||||
|
||||
|
||||
@app.delete("/users/", summary="批量删除用户")
|
||||
async def delete_users(ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
@app.delete("/users/", summary="批量删除用户", description="软删除,删除后清空所关联的角色")
|
||||
async def delete_users(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.delete"]))):
|
||||
if auth.user.id in ids.ids:
|
||||
return ErrorResponse("不能删除当前登录用户")
|
||||
elif 1 in ids.ids:
|
||||
return ErrorResponse("不能删除超级管理员用户")
|
||||
await crud.UserDal(auth.db).delete_datas(ids=ids.ids, soft=True)
|
||||
await crud.UserDal(auth.db).delete_datas(ids=ids.ids, v_soft=True, is_active=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/users/{data_id}/", summary="更新用户信息")
|
||||
async def put_user(data_id: int, data: schemas.User, auth: Auth = Depends(login_auth)):
|
||||
async def put_user(
|
||||
data_id: int,
|
||||
data: schemas.UserUpdate,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.update"]))
|
||||
):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/users/{data_id}/", summary="获取用户信息")
|
||||
async def get_user(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_user(
|
||||
data_id: int,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.view", "auth.user.update"]))
|
||||
):
|
||||
model = models.VadminUser
|
||||
options = [model.roles]
|
||||
options = [joinedload(model.roles)]
|
||||
schema = schemas.UserOut
|
||||
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, options, v_schema=schema))
|
||||
|
||||
|
||||
@app.post("/user/current/reset/password/", summary="重置当前用户密码")
|
||||
async def user_current_reset_password(data: schemas.ResetPwd, auth: Auth = Depends(login_auth)):
|
||||
async def user_current_reset_password(data: schemas.ResetPwd, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).reset_current_password(auth.user, data))
|
||||
|
||||
|
||||
@app.post("/user/current/update/info/", summary="更新当前用户基本信息")
|
||||
async def post_user_current_update_info(data: schemas.UserUpdate, auth: Auth = Depends(login_auth)):
|
||||
async def post_user_current_update_info(data: schemas.UserUpdateBaseInfo, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).update_current_info(auth.user, data))
|
||||
|
||||
|
||||
@app.post("/user/current/update/avatar/", summary="更新当前用户头像")
|
||||
async def post_user_current_update_avatar(file: UploadFile, auth: Auth = Depends(login_auth)):
|
||||
async def post_user_current_update_avatar(file: UploadFile, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).update_current_avatar(auth.user, file))
|
||||
|
||||
|
||||
@app.get("/user/current/info/", summary="获取当前用户基本信息")
|
||||
async def get_user_current_info(auth: Auth = Depends(full_admin)):
|
||||
@app.get("/user/admin/current/info/", summary="获取当前管理员信息")
|
||||
async def get_user_admin_current_info(auth: Auth = Depends(FullAdminAuth())):
|
||||
result = schemas.UserOut.from_orm(auth.user).dict()
|
||||
result["permissions"] = await get_user_permissions(auth.user)
|
||||
result["permissions"] = list(get_user_permissions(auth.user))
|
||||
return SuccessResponse(result)
|
||||
|
||||
|
||||
@ -80,65 +95,85 @@ async def get_user_current_info(auth: Auth = Depends(full_admin)):
|
||||
async def post_user_export_query_list(
|
||||
header: list = Body(..., title="表头与对应字段"),
|
||||
params: UserParams = Depends(),
|
||||
auth: Auth = Depends(login_auth)
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.export"]))
|
||||
):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).export_query_list(header, params))
|
||||
|
||||
|
||||
@app.get("/user/download/import/template/", summary="下载最新批量导入用户模板")
|
||||
async def get_user_download_new_import_template(auth: Auth = Depends(login_auth)):
|
||||
async def get_user_download_new_import_template(auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).download_import_template())
|
||||
|
||||
|
||||
@app.post("/import/users/", summary="批量导入用户")
|
||||
async def post_import_users(file: UploadFile, auth: Auth = Depends(login_auth)):
|
||||
async def post_import_users(file: UploadFile, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.import"]))):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).import_users(file))
|
||||
|
||||
|
||||
@app.post("/users/init/password/send/sms/", summary="初始化所选用户密码并发送通知短信")
|
||||
async def post_users_init_password(request: Request, ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def post_users_init_password(
|
||||
request: Request,
|
||||
ids: IdList = Depends(),
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"]))
|
||||
):
|
||||
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_sms(ids.ids, request.app.state.redis))
|
||||
|
||||
|
||||
@app.put("/users/wx/server/openid/", summary="更新当前用户服务端微信平台openid")
|
||||
async def put_user_wx_server_openid(request: Request, code: str, auth: Auth = Depends(AllUserAuth())):
|
||||
result = await crud.UserDal(auth.db).update_wx_server_openid(code, auth.user, request.app.state.redis)
|
||||
return SuccessResponse(result)
|
||||
|
||||
|
||||
###########################################################
|
||||
# 角色管理
|
||||
###########################################################
|
||||
@app.get("/roles/", summary="获取角色列表")
|
||||
async def get_roles(params: RoleParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def get_roles(
|
||||
params: RoleParams = Depends(),
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.list"]))
|
||||
):
|
||||
datas = await crud.RoleDal(auth.db).get_datas(**params.dict())
|
||||
count = await crud.RoleDal(auth.db).get_count(**params.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.post("/roles/", summary="创建角色信息")
|
||||
async def create_role(role: schemas.RoleIn, auth: Auth = Depends(login_auth)):
|
||||
async def create_role(role: schemas.RoleIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create"]))):
|
||||
return SuccessResponse(await crud.RoleDal(auth.db).create_data(data=role))
|
||||
|
||||
|
||||
@app.delete("/roles/", summary="批量删除角色")
|
||||
async def delete_roles(ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
@app.delete("/roles/", summary="批量删除角色", description="硬删除, 如果存在用户关联则无法删除")
|
||||
async def delete_roles(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.delete"]))):
|
||||
if 1 in ids.ids:
|
||||
return ErrorResponse("不能删除管理员角色")
|
||||
await crud.RoleDal(auth.db).delete_datas(ids.ids, soft=True)
|
||||
await crud.RoleDal(auth.db).delete_datas(ids.ids, v_soft=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/roles/{data_id}/", summary="更新角色信息")
|
||||
async def put_role(data_id: int, data: schemas.RoleIn, auth: Auth = Depends(login_auth)):
|
||||
async def put_role(
|
||||
data_id: int,
|
||||
data: schemas.RoleIn,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.update"]))
|
||||
):
|
||||
if 1 == data_id:
|
||||
return ErrorResponse("不能修改管理员角色")
|
||||
return SuccessResponse(await crud.RoleDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/roles/options/", summary="获取角色选择项")
|
||||
async def get_role_options(auth: Auth = Depends(login_auth)):
|
||||
async def get_role_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create", "auth.user.update"]))):
|
||||
return SuccessResponse(await crud.RoleDal(auth.db).get_select_datas())
|
||||
|
||||
|
||||
@app.get("/roles/{data_id}/", summary="获取角色信息")
|
||||
async def get_role(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_role(
|
||||
data_id: int,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.view", "auth.role.update"]))
|
||||
):
|
||||
model = models.VadminRole
|
||||
options = [model.menus]
|
||||
options = [joinedload(model.menus)]
|
||||
schema = schemas.RoleOut
|
||||
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, options, v_schema=schema))
|
||||
|
||||
@ -147,48 +182,59 @@ async def get_role(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
# 菜单管理
|
||||
###########################################################
|
||||
@app.get("/menus/", summary="获取菜单列表")
|
||||
async def get_menus(auth: Auth = Depends(login_auth)):
|
||||
async def get_menus(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.list"]))):
|
||||
datas = await crud.MenuDal(auth.db).get_tree_list(mode=1)
|
||||
return SuccessResponse(datas)
|
||||
|
||||
|
||||
@app.get("/menus/tree/options/", summary="获取菜单树选择项,添加/修改菜单时使用")
|
||||
async def get_menus_options(auth: Auth = Depends(login_auth)):
|
||||
async def get_menus_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create", "auth.menu.update"]))):
|
||||
datas = await crud.MenuDal(auth.db).get_tree_list(mode=2)
|
||||
return SuccessResponse(datas)
|
||||
|
||||
|
||||
@app.get("/menus/role/tree/options/", summary="获取菜单列表树信息,角色权限使用")
|
||||
async def get_menus_treeselect(auth: Auth = Depends(login_auth)):
|
||||
async def get_menus_treeselect(
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
|
||||
):
|
||||
return SuccessResponse(await crud.MenuDal(auth.db).get_tree_list(mode=3))
|
||||
|
||||
|
||||
@app.post("/menus/", summary="创建菜单信息")
|
||||
async def create_menu(menu: schemas.Menu, auth: Auth = Depends(login_auth)):
|
||||
async def create_menu(menu: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create"]))):
|
||||
if menu.parent_id:
|
||||
menu.alwaysShow = False
|
||||
return SuccessResponse(await crud.MenuDal(auth.db).create_data(data=menu))
|
||||
|
||||
|
||||
@app.delete("/menus/", summary="批量删除菜单")
|
||||
async def delete_menus(ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
await crud.MenuDal(auth.db).delete_datas(ids.ids, soft=True)
|
||||
@app.delete("/menus/", summary="批量删除菜单", description="硬删除, 如果存在角色关联则无法删除")
|
||||
async def delete_menus(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.delete"]))):
|
||||
await crud.MenuDal(auth.db).delete_datas(ids.ids, v_soft=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/menus/{data_id}/", summary="更新菜单信息")
|
||||
async def put_menus(data_id: int, data: schemas.Menu, auth: Auth = Depends(login_auth)):
|
||||
async def put_menus(
|
||||
data_id: int,
|
||||
data: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.update"]))
|
||||
):
|
||||
return SuccessResponse(await crud.MenuDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/menus/{data_id}/", summary="获取菜单信息")
|
||||
async def put_menus(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def put_menus(
|
||||
data_id: int,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
|
||||
):
|
||||
schema = schemas.MenuSimpleOut
|
||||
return SuccessResponse(await crud.MenuDal(auth.db).get_data(data_id, None, v_schema=schema))
|
||||
|
||||
|
||||
@app.get("/role/menus/tree/{role_id}/", summary="获取菜单列表树信息以及角色菜单权限ID,角色权限使用")
|
||||
async def get_role_menu_tree(role_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_role_menu_tree(
|
||||
role_id: int,
|
||||
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
|
||||
):
|
||||
treeselect = await crud.MenuDal(auth.db).get_tree_list(mode=3)
|
||||
role_menu_tree = await crud.RoleDal(auth.db).get_role_menu_tree(role_id)
|
||||
return SuccessResponse({"role_menu_tree": role_menu_tree, "menus": treeselect})
|
||||
|
7
kinit-api/apps/vadmin/help/__init__.py
Normal file
7
kinit-api/apps/vadmin/help/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 帮助中心
|
34
kinit-api/apps/vadmin/help/crud.py
Normal file
34
kinit-api/apps/vadmin/help/crud.py
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : crud.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 帮助中心 - 增删改查
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.crud import DalBase
|
||||
from . import models, schemas
|
||||
|
||||
|
||||
class IssueDal(DalBase):
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
super(IssueDal, self).__init__(db, models.VadminIssue, schemas.IssueSimpleOut)
|
||||
|
||||
async def add_view_number(self, data_id: int):
|
||||
"""
|
||||
更新常见问题查看次数+1
|
||||
"""
|
||||
obj = await self.get_data(data_id)
|
||||
obj.view_number = obj.view_number + 1 if obj.view_number else 1
|
||||
await self.flush(obj)
|
||||
return True
|
||||
|
||||
|
||||
class IssueCategoryDal(DalBase):
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
super(IssueCategoryDal, self).__init__(db, models.VadminIssueCategory, schemas.IssueCategorySimpleOut)
|
||||
|
10
kinit-api/apps/vadmin/help/models/__init__.py
Normal file
10
kinit-api/apps/vadmin/help/models/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 初始化文件
|
||||
|
||||
|
||||
from .issue import VadminIssue, VadminIssueCategory
|
42
kinit-api/apps/vadmin/help/models/issue.py
Normal file
42
kinit-api/apps/vadmin/help/models/issue.py
Normal file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/7/7 13:41
|
||||
# @File : issue.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 常见问题
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from db.db_base import BaseModel
|
||||
from sqlalchemy import Column, String, Boolean, Integer, ForeignKey, Text
|
||||
|
||||
|
||||
class VadminIssueCategory(BaseModel):
|
||||
__tablename__ = "vadmin_help_issue_category"
|
||||
__table_args__ = ({'comment': '常见问题类别表'})
|
||||
|
||||
name = Column(String(50), index=True, nullable=False, comment="类别名称")
|
||||
platform = Column(String(8), index=True, nullable=False, comment="展示平台")
|
||||
is_active = Column(Boolean, default=True, comment="是否可见")
|
||||
|
||||
issues = relationship("VadminIssue", back_populates='category')
|
||||
|
||||
user_id = Column(ForeignKey("vadmin_auth_user.id", ondelete='SET NULL'), comment="创建人")
|
||||
user = relationship("VadminUser", foreign_keys=user_id)
|
||||
|
||||
|
||||
class VadminIssue(BaseModel):
|
||||
__tablename__ = "vadmin_help_issue"
|
||||
__table_args__ = ({'comment': '常见问题记录表'})
|
||||
|
||||
category_id = Column(ForeignKey("vadmin_help_issue_category.id", ondelete='CASCADE'), comment="类别")
|
||||
category = relationship("VadminIssueCategory", foreign_keys=category_id, back_populates='issues')
|
||||
|
||||
title = Column(String(255), index=True, nullable=False, comment="标题")
|
||||
content = Column(Text, comment="内容")
|
||||
view_number = Column(Integer, default=0, comment="查看次数")
|
||||
is_active = Column(Boolean, default=True, comment="是否可见")
|
||||
|
||||
user_id = Column(ForeignKey("vadmin_auth_user.id", ondelete='SET NULL'), comment="创建人")
|
||||
user = relationship("VadminUser", foreign_keys=user_id)
|
||||
|
10
kinit-api/apps/vadmin/help/params/__init__.py
Normal file
10
kinit-api/apps/vadmin/help/params/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 初始化文件
|
||||
|
||||
|
||||
from .issue import IssueParams, IssueCategoryParams
|
51
kinit-api/apps/vadmin/help/params/issue.py
Normal file
51
kinit-api/apps/vadmin/help/params/issue.py
Normal file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/7/7 13:41
|
||||
# @File : issue.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 常见问题
|
||||
|
||||
|
||||
from fastapi import Depends
|
||||
from core.dependencies import Paging, QueryParams
|
||||
|
||||
|
||||
class IssueParams(QueryParams):
|
||||
"""
|
||||
列表分页
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: Paging = Depends(),
|
||||
is_active: bool = None,
|
||||
title: str = None,
|
||||
category_id: int = None
|
||||
):
|
||||
super().__init__(params)
|
||||
self.v_order = "desc"
|
||||
self.v_order_field = "create_datetime"
|
||||
self.is_active = is_active
|
||||
self.category_id = category_id
|
||||
self.title = ("like", title)
|
||||
|
||||
|
||||
class IssueCategoryParams(QueryParams):
|
||||
"""
|
||||
列表分页
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: Paging = Depends(),
|
||||
is_active: bool = None,
|
||||
platform: str = None,
|
||||
name: str = None
|
||||
):
|
||||
super().__init__(params)
|
||||
self.v_order = "desc"
|
||||
self.v_order_field = "create_datetime"
|
||||
self.is_active = is_active
|
||||
self.platform = platform
|
||||
self.name = ("like", name)
|
12
kinit-api/apps/vadmin/help/schemas/__init__.py
Normal file
12
kinit-api/apps/vadmin/help/schemas/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 初始化文件
|
||||
|
||||
|
||||
from .issue import Issue, IssueSimpleOut, IssueListOut
|
||||
from .issue_category import IssueCategory, IssueCategorySimpleOut, IssueCategoryListOut, IssueCategoryOptionsOut
|
||||
from .issue_m2m import IssueCategoryPlatformOut
|
40
kinit-api/apps/vadmin/help/schemas/issue.py
Normal file
40
kinit-api/apps/vadmin/help/schemas/issue.py
Normal file
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/7/7 13:41
|
||||
# @File : issue.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 常见问题
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from core.data_types import DatetimeStr
|
||||
from apps.vadmin.auth.schemas import UserSimpleOut
|
||||
from .issue_category import IssueCategorySimpleOut
|
||||
|
||||
|
||||
class Issue(BaseModel):
|
||||
category_id: Optional[int] = None
|
||||
user_id: Optional[int] = None
|
||||
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
view_number: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class IssueSimpleOut(Issue):
|
||||
id: int
|
||||
update_datetime: DatetimeStr
|
||||
create_datetime: DatetimeStr
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class IssueListOut(IssueSimpleOut):
|
||||
user: UserSimpleOut
|
||||
category: IssueCategorySimpleOut
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
45
kinit-api/apps/vadmin/help/schemas/issue_category.py
Normal file
45
kinit-api/apps/vadmin/help/schemas/issue_category.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/7/7 13:41
|
||||
# @File : issue_category.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 常见问题类别
|
||||
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from core.data_types import DatetimeStr
|
||||
from apps.vadmin.auth.schemas import UserSimpleOut
|
||||
|
||||
|
||||
class IssueCategory(BaseModel):
|
||||
name: Optional[str] = None
|
||||
platform: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
user_id: Optional[int] = None
|
||||
|
||||
|
||||
class IssueCategorySimpleOut(IssueCategory):
|
||||
id: int
|
||||
update_datetime: DatetimeStr
|
||||
create_datetime: DatetimeStr
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class IssueCategoryListOut(IssueCategorySimpleOut):
|
||||
user: UserSimpleOut
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class IssueCategoryOptionsOut(BaseModel):
|
||||
label: str = Field(alias='name')
|
||||
value: int = Field(alias='id')
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
29
kinit-api/apps/vadmin/help/schemas/issue_m2m.py
Normal file
29
kinit-api/apps/vadmin/help/schemas/issue_m2m.py
Normal file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023/2/17 15:18
|
||||
# @File : issue_m2m.py.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
from core.data_types import DatetimeStr
|
||||
from .issue import IssueSimpleOut
|
||||
|
||||
|
||||
class IssueCategoryPlatformOut(BaseModel):
|
||||
name: Optional[str] = None
|
||||
platform: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
user_id: Optional[int] = None
|
||||
|
||||
id: int
|
||||
update_datetime: DatetimeStr
|
||||
create_datetime: DatetimeStr
|
||||
|
||||
issues: Optional[List[IssueSimpleOut]] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
112
kinit-api/apps/vadmin/help/views.py
Normal file
112
kinit-api/apps/vadmin/help/views.py
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023-02-15 20:03:49
|
||||
# @File : views.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 帮助中心视图
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
from core.database import db_getter
|
||||
from utils.response import SuccessResponse
|
||||
from . import schemas, crud, params, models
|
||||
from core.dependencies import IdList
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
|
||||
###########################################################
|
||||
# 类别管理
|
||||
###########################################################
|
||||
@app.get("/issue/categorys/", summary="获取类别列表")
|
||||
async def get_issue_categorys(p: params.IssueCategoryParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
model = models.VadminIssueCategory
|
||||
options = [joinedload(model.user)]
|
||||
schema = schemas.IssueCategoryListOut
|
||||
datas = await crud.IssueCategoryDal(auth.db).get_datas(**p.dict(), v_options=options, v_schema=schema)
|
||||
count = await crud.IssueCategoryDal(auth.db).get_count(**p.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.get("/issue/categorys/options/", summary="获取类别选择项")
|
||||
async def get_issue_categorys_options(auth: Auth = Depends(AllUserAuth())):
|
||||
schema = schemas.IssueCategoryOptionsOut
|
||||
return SuccessResponse(await crud.IssueCategoryDal(auth.db).get_datas(limit=0, is_active=True, v_schema=schema))
|
||||
|
||||
|
||||
@app.post("/issue/categorys/", summary="创建类别")
|
||||
async def create_issue_category(data: schemas.IssueCategory, auth: Auth = Depends(AllUserAuth())):
|
||||
data.user_id = auth.user.id
|
||||
return SuccessResponse(await crud.IssueCategoryDal(auth.db).create_data(data=data))
|
||||
|
||||
|
||||
@app.delete("/issue/categorys/", summary="批量删除类别", description="硬删除")
|
||||
async def delete_issue_categorys(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
await crud.IssueCategoryDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/issue/categorys/{data_id}/", summary="更新类别信息")
|
||||
async def put_issue_category(data_id: int, data: schemas.IssueCategory, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.IssueCategoryDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/issue/categorys/{data_id}/", summary="获取类别信息")
|
||||
async def get_issue_category(data_id: int, auth: Auth = Depends(AllUserAuth())):
|
||||
schema = schemas.IssueCategorySimpleOut
|
||||
return SuccessResponse(await crud.IssueCategoryDal(auth.db).get_data(data_id, v_schema=schema))
|
||||
|
||||
|
||||
@app.get("/issue/categorys/platform/{platform}/", summary="获取平台中的常见问题类别列表")
|
||||
async def get_issue_category_platform(platform: str, db: AsyncSession = Depends(db_getter)):
|
||||
model = models.VadminIssueCategory
|
||||
options = [joinedload(model.issues)]
|
||||
schema = schemas.IssueCategoryPlatformOut
|
||||
result = await crud.IssueCategoryDal(db).\
|
||||
get_datas(platform=platform, is_active=True, v_schema=schema, v_options=options)
|
||||
return SuccessResponse(result)
|
||||
|
||||
|
||||
###########################################################
|
||||
# 问题管理
|
||||
###########################################################
|
||||
@app.get("/issues/", summary="获取问题列表")
|
||||
async def get_issues(p: params.IssueParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
model = models.VadminIssue
|
||||
options = [joinedload(model.user), joinedload(model.category)]
|
||||
schema = schemas.IssueListOut
|
||||
datas = await crud.IssueDal(auth.db).get_datas(**p.dict(), v_options=options, v_schema=schema)
|
||||
count = await crud.IssueDal(auth.db).get_count(**p.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.post("/issues/", summary="创建问题")
|
||||
async def create_issue(data: schemas.Issue, auth: Auth = Depends(AllUserAuth())):
|
||||
data.user_id = auth.user.id
|
||||
return SuccessResponse(await crud.IssueDal(auth.db).create_data(data=data))
|
||||
|
||||
|
||||
@app.delete("/issues/", summary="批量删除问题", description="硬删除")
|
||||
async def delete_issues(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
await crud.IssueDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/issues/{data_id}/", summary="更新问题信息")
|
||||
async def put_issue(data_id: int, data: schemas.Issue, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.IssueDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/issues/{data_id}/", summary="获取问题信息")
|
||||
async def get_issue(data_id: int, db: AsyncSession = Depends(db_getter)):
|
||||
schema = schemas.IssueSimpleOut
|
||||
return SuccessResponse(await crud.IssueDal(db).get_data(data_id, v_schema=schema))
|
||||
|
||||
|
||||
@app.get("/issues/add/view/number/{data_id}/", summary="更新常见问题查看次数+1")
|
||||
async def issue_add_view_number(data_id: int, db: AsyncSession = Depends(db_getter)):
|
||||
return SuccessResponse(await crud.IssueDal(db).add_view_number(data_id))
|
@ -32,7 +32,7 @@ class LoginRecordDal(DalBase):
|
||||
total: 20
|
||||
}
|
||||
|
||||
@return: List[dict]
|
||||
:return: List[dict]
|
||||
"""
|
||||
result = [{
|
||||
"name": '北京',
|
||||
|
@ -8,7 +8,7 @@
|
||||
import json
|
||||
|
||||
from application.settings import LOGIN_LOG_RECORD
|
||||
from apps.vadmin.auth.utils.validation import LoginForm
|
||||
from apps.vadmin.auth.utils.validation import LoginForm, WXLoginForm
|
||||
from utils.ip_manage import IPManage
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from db.db_base import BaseModel
|
||||
@ -21,7 +21,7 @@ class VadminLoginRecord(BaseModel):
|
||||
__tablename__ = "vadmin_record_login"
|
||||
__table_args__ = ({'comment': '登录记录表'})
|
||||
|
||||
telephone = Column(String(50), index=True, nullable=False, comment="手机号")
|
||||
telephone = Column(String(255), index=True, nullable=False, comment="手机号")
|
||||
status = Column(Boolean, default=True, comment="是否登录成功")
|
||||
platform = Column(String(8), comment="登陆平台")
|
||||
login_method = Column(String(8), comment="认证方式")
|
||||
@ -40,10 +40,17 @@ class VadminLoginRecord(BaseModel):
|
||||
request = Column(TEXT, comment="请求信息")
|
||||
|
||||
@classmethod
|
||||
async def create_login_record(cls, db: AsyncSession, data: LoginForm, status: bool, req: Request, resp: dict):
|
||||
async def create_login_record(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
data: LoginForm | WXLoginForm,
|
||||
status: bool,
|
||||
req: Request,
|
||||
resp: dict
|
||||
):
|
||||
"""
|
||||
创建登录记录
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
if not LOGIN_LOG_RECORD:
|
||||
return None
|
||||
@ -59,7 +66,7 @@ class VadminLoginRecord(BaseModel):
|
||||
params = json.dumps({"body": body, "headers": header})
|
||||
obj = VadminLoginRecord(
|
||||
**location.dict(),
|
||||
telephone=data.telephone,
|
||||
telephone=data.telephone if data.telephone else data.code,
|
||||
status=status,
|
||||
browser=browser,
|
||||
system=system,
|
||||
|
@ -8,7 +8,8 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from utils.response import SuccessResponse
|
||||
from . import crud, schemas
|
||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
from core.mongo import get_database, DatabaseManage
|
||||
from .params import LoginParams, OperationParams, SMSParams
|
||||
|
||||
@ -19,7 +20,7 @@ app = APIRouter()
|
||||
# 日志管理
|
||||
###########################################################
|
||||
@app.get("/logins/", summary="获取登录日志列表")
|
||||
async def get_record_login(p: LoginParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def get_record_login(p: LoginParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
datas = await crud.LoginRecordDal(auth.db).get_datas(**p.dict())
|
||||
count = await crud.LoginRecordDal(auth.db).get_count(**p.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
@ -27,14 +28,14 @@ async def get_record_login(p: LoginParams = Depends(), auth: Auth = Depends(logi
|
||||
|
||||
@app.get("/operations/", summary="获取操作日志列表")
|
||||
async def get_record_operation(p: OperationParams = Depends(), db: DatabaseManage = Depends(get_database),
|
||||
auth: Auth = Depends(login_auth)):
|
||||
auth: Auth = Depends(AllUserAuth())):
|
||||
count = await db.get_count("operation_record", **p.to_count())
|
||||
datas = await db.get_datas("operation_record", v_schema=schemas.OpertionRecordSimpleOut, **p.dict())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.get("/sms/send/list/", summary="获取短信发送列表")
|
||||
async def get_sms_send_list(p: SMSParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def get_sms_send_list(p: SMSParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
datas = await crud.SMSSendRecordDal(auth.db).get_datas(**p.dict())
|
||||
count = await crud.SMSSendRecordDal(auth.db).get_count(**p.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
@ -44,5 +45,5 @@ async def get_sms_send_list(p: SMSParams = Depends(), auth: Auth = Depends(login
|
||||
# 日志分析
|
||||
###########################################################
|
||||
@app.get("/analysis/user/login/distribute/", summary="获取用户登录分布情况列表")
|
||||
async def get_user_login_distribute(auth: Auth = Depends(login_auth)):
|
||||
async def get_user_login_distribute(auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.LoginRecordDal(auth.db).get_user_distribute())
|
||||
|
@ -9,10 +9,14 @@
|
||||
# sqlalchemy 查询操作:https://segmentfault.com/a/1190000016767008
|
||||
# sqlalchemy 关联查询:https://www.jianshu.com/p/dfad7c08c57a
|
||||
# sqlalchemy 关联查询详细:https://blog.csdn.net/u012324798/article/details/103940527
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from aioredis import Redis
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
from application.settings import STATIC_ROOT
|
||||
from utils.file.file_manage import FileManage
|
||||
from . import models, schemas
|
||||
@ -29,14 +33,15 @@ class DictTypeDal(DalBase):
|
||||
获取多个字典类型下的字典元素列表
|
||||
"""
|
||||
data = {}
|
||||
for dict_type in dict_types:
|
||||
dict_data = await DictTypeDal(self.db)\
|
||||
.get_data(dict_type=dict_type, v_return_none=True, v_options=[self.model.details])
|
||||
if not dict_data:
|
||||
data[dict_type] = []
|
||||
options = [joinedload(self.model.details)]
|
||||
objs = await DictTypeDal(self.db).\
|
||||
get_datas(limit=0, v_return_objs=True, v_options=options, dict_type=("in", dict_types))
|
||||
for obj in objs:
|
||||
if not obj:
|
||||
data[obj.dict_type] = []
|
||||
continue
|
||||
else:
|
||||
data[dict_type] = [schemas.DictDetailsSimpleOut.from_orm(i).dict() for i in dict_data.details]
|
||||
data[obj.dict_type] = [schemas.DictDetailsSimpleOut.from_orm(i).dict() for i in obj.details]
|
||||
return data
|
||||
|
||||
async def get_select_datas(self):
|
||||
@ -68,7 +73,7 @@ class SettingsDal(DalBase):
|
||||
result[data.config_key] = data.config_value
|
||||
return result
|
||||
|
||||
async def update_datas(self, datas: dict):
|
||||
async def update_datas(self, datas: dict, rd: Redis):
|
||||
"""
|
||||
更新系统配置信息
|
||||
|
||||
@ -92,6 +97,20 @@ class SettingsDal(DalBase):
|
||||
else:
|
||||
sql = update(self.model).where(self.model.config_key == key).values(config_value=value)
|
||||
await self.db.execute(sql)
|
||||
if "wx_server_app_id" in datas:
|
||||
await rd.client().set("wx_server", json.dumps(datas))
|
||||
|
||||
async def get_base_config(self):
|
||||
"""
|
||||
获取系统基本信息
|
||||
"""
|
||||
ignore_configs = ["wx_server_app_id", "wx_server_app_secret"]
|
||||
datas = await self.get_datas(limit=0, tab_id=("in", ["1", "9"]), disabled=False, v_return_objs=True)
|
||||
result = {}
|
||||
for config in datas:
|
||||
if config.config_key not in ignore_configs:
|
||||
result[config.config_key] = config.config_value
|
||||
return result
|
||||
|
||||
|
||||
class SettingsTabDal(DalBase):
|
||||
@ -99,20 +118,43 @@ class SettingsTabDal(DalBase):
|
||||
def __init__(self, db: AsyncSession):
|
||||
super(SettingsTabDal, self).__init__(db, models.VadminSystemSettingsTab, schemas.SettingsTabSimpleOut)
|
||||
|
||||
async def get_classify_tab_values(self, classify: List[str], hidden: bool | None = False):
|
||||
async def get_classify_tab_values(self, classify: List[str], hidden: Union[bool, None] = False):
|
||||
"""
|
||||
获取系统配置分类下的所有显示标签信息
|
||||
获取系统配置分类下的标签信息
|
||||
"""
|
||||
model = models.VadminSystemSettingsTab
|
||||
options = [model.settings]
|
||||
options = [joinedload(model.settings)]
|
||||
datas = await self.get_datas(
|
||||
limit=0,
|
||||
v_options=options,
|
||||
classify=("in", classify),
|
||||
disabled=False,
|
||||
hidden=hidden,
|
||||
v_return_objs=True
|
||||
v_return_objs=True,
|
||||
hidden=hidden
|
||||
)
|
||||
return self.generate_values(datas)
|
||||
|
||||
async def get_tab_name_values(self, tab_names: List[str], hidden: Union[bool, None] = False):
|
||||
"""
|
||||
获取系统配置标签下的标签信息
|
||||
"""
|
||||
model = models.VadminSystemSettingsTab
|
||||
options = [joinedload(model.settings)]
|
||||
datas = await self.get_datas(
|
||||
limit=0,
|
||||
v_options=options,
|
||||
tab_name=("in", tab_names),
|
||||
disabled=False,
|
||||
v_return_objs=True,
|
||||
hidden=hidden
|
||||
)
|
||||
return self.generate_values(datas)
|
||||
|
||||
@classmethod
|
||||
def generate_values(cls, datas: List[models.VadminSystemSettingsTab]):
|
||||
"""
|
||||
生成字典值
|
||||
"""
|
||||
result = {}
|
||||
for tab in datas:
|
||||
tabs = {}
|
||||
@ -121,3 +163,6 @@ class SettingsTabDal(DalBase):
|
||||
tabs[item.config_key] = item.config_value
|
||||
result[tab.tab_name] = tabs
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ class DictTypeParams(QueryParams):
|
||||
"""
|
||||
列表分页
|
||||
"""
|
||||
def __init__(self, dict_name: str = None, params: Paging = Depends()):
|
||||
def __init__(self, dict_name: str = None, dict_type: str = None, params: Paging = Depends()):
|
||||
super().__init__(params)
|
||||
self.dict_name = ("like", dict_name)
|
||||
self.dict_type = ("like", dict_type)
|
||||
|
@ -17,7 +17,8 @@ from utils.file.file_manage import FileManage
|
||||
from utils.response import SuccessResponse, ErrorResponse
|
||||
from . import schemas, crud
|
||||
from core.dependencies import IdList
|
||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
from .params import DictTypeParams, DictDetailParams
|
||||
|
||||
app = APIRouter()
|
||||
@ -27,26 +28,26 @@ app = APIRouter()
|
||||
# 字典类型管理
|
||||
###########################################################
|
||||
@app.get("/dict/types/", summary="获取字典类型列表")
|
||||
async def get_dict_types(p: DictTypeParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def get_dict_types(p: DictTypeParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
datas = await crud.DictTypeDal(auth.db).get_datas(**p.dict())
|
||||
count = await crud.DictTypeDal(auth.db).get_count(**p.to_count())
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.post("/dict/types/", summary="创建字典类型")
|
||||
async def create_dict_types(data: schemas.DictType, auth: Auth = Depends(login_auth)):
|
||||
async def create_dict_types(data: schemas.DictType, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.DictTypeDal(auth.db).create_data(data=data))
|
||||
|
||||
|
||||
@app.delete("/dict/types/", summary="批量删除字典类型")
|
||||
async def delete_dict_types(ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def delete_dict_types(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
await crud.DictTypeDal(auth.db).delete_datas(ids=ids.ids)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.post("/dict/types/details/", summary="获取多个字典类型下的字典元素列表")
|
||||
async def post_dicts_details(
|
||||
auth: Auth = Depends(login_auth),
|
||||
auth: Auth = Depends(AllUserAuth()),
|
||||
dict_types: List[str] = Body(None, title="字典元素列表", description="查询字典元素列表")
|
||||
):
|
||||
datas = await crud.DictTypeDal(auth.db).get_dicts_details(dict_types)
|
||||
@ -54,17 +55,17 @@ async def post_dicts_details(
|
||||
|
||||
|
||||
@app.get("/dict/types/options/", summary="获取字典类型选择项")
|
||||
async def get_dicts_options(auth: Auth = Depends(login_auth)):
|
||||
async def get_dicts_options(auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.DictTypeDal(auth.db).get_select_datas())
|
||||
|
||||
|
||||
@app.put("/dict/types/{data_id}/", summary="更新字典类型")
|
||||
async def put_dict_types(data_id: int, data: schemas.DictType, auth: Auth = Depends(login_auth)):
|
||||
async def put_dict_types(data_id: int, data: schemas.DictType, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.DictTypeDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/dict/types/{data_id}/", summary="获取字典类型详细")
|
||||
async def get_dict_type(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_dict_type(data_id: int, auth: Auth = Depends(AllUserAuth())):
|
||||
schema = schemas.DictTypeSimpleOut
|
||||
return SuccessResponse(await crud.DictTypeDal(auth.db).get_data(data_id, None, v_schema=schema))
|
||||
|
||||
@ -73,12 +74,12 @@ async def get_dict_type(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
# 字典元素管理
|
||||
###########################################################
|
||||
@app.post("/dict/details/", summary="创建字典元素")
|
||||
async def create_dict_details(data: schemas.DictDetails, auth: Auth = Depends(login_auth)):
|
||||
async def create_dict_details(data: schemas.DictDetails, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.DictDetailsDal(auth.db).create_data(data=data))
|
||||
|
||||
|
||||
@app.get("/dict/details/", summary="获取单个字典类型下的字典元素列表,分页")
|
||||
async def get_dict_details(params: DictDetailParams = Depends(), auth: Auth = Depends(login_auth)):
|
||||
async def get_dict_details(params: DictDetailParams = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
if not params.dict_type_id:
|
||||
return ErrorResponse(msg="未获取到字典类型!")
|
||||
datas = await crud.DictDetailsDal(auth.db).get_datas(**params.dict())
|
||||
@ -86,19 +87,19 @@ async def get_dict_details(params: DictDetailParams = Depends(), auth: Auth = De
|
||||
return SuccessResponse(datas, count=count)
|
||||
|
||||
|
||||
@app.delete("/dict/details/", summary="批量删除字典元素")
|
||||
async def delete_dict_details(ids: IdList = Depends(), auth: Auth = Depends(login_auth)):
|
||||
await crud.DictDetailsDal(auth.db).delete_datas(ids.ids)
|
||||
@app.delete("/dict/details/", summary="批量删除字典元素", description="硬删除")
|
||||
async def delete_dict_details(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
|
||||
await crud.DictDetailsDal(auth.db).delete_datas(ids.ids, v_soft=False)
|
||||
return SuccessResponse("删除成功")
|
||||
|
||||
|
||||
@app.put("/dict/details/{data_id}/", summary="更新字典元素")
|
||||
async def put_dict_details(data_id: int, data: schemas.DictDetails, auth: Auth = Depends(login_auth)):
|
||||
async def put_dict_details(data_id: int, data: schemas.DictDetails, auth: Auth = Depends(AllUserAuth())):
|
||||
return SuccessResponse(await crud.DictDetailsDal(auth.db).put_data(data_id, data))
|
||||
|
||||
|
||||
@app.get("/dict/details/{data_id}/", summary="获取字典元素详情")
|
||||
async def get_dict_detail(data_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_dict_detail(data_id: int, auth: Auth = Depends(AllUserAuth())):
|
||||
schema = schemas.DictDetailsSimpleOut
|
||||
return SuccessResponse(await crud.DictDetailsDal(auth.db).get_data(data_id, None, v_schema=schema))
|
||||
|
||||
@ -134,25 +135,30 @@ async def sms_send(request: Request, telephone: str):
|
||||
# 系统配置管理
|
||||
###########################################################
|
||||
@app.get("/settings/tabs/", summary="获取系统配置标签列表")
|
||||
async def get_settings_tabs(classify: str, auth: Auth = Depends(login_auth)):
|
||||
async def get_settings_tabs(classify: str, auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse(await crud.SettingsTabDal(auth.db).get_datas(limit=0, classify=classify))
|
||||
|
||||
|
||||
@app.get("/settings/tabs/values/", summary="获取系统配置标签下的信息")
|
||||
async def get_settings_tabs_values(tab_id: int, auth: Auth = Depends(login_auth)):
|
||||
async def get_settings_tabs_values(tab_id: int, auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse(await crud.SettingsDal(auth.db).get_tab_values(tab_id=tab_id))
|
||||
|
||||
|
||||
@app.put("/settings/tabs/values/", summary="更新系统配置信息")
|
||||
async def put_settings_tabs_values(datas: dict = Body(...), auth: Auth = Depends(login_auth)):
|
||||
return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas))
|
||||
async def put_settings_tabs_values(request: Request, datas: dict = Body(...), auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas, request.app.state.redis))
|
||||
|
||||
|
||||
@app.get("/settings/classifys/", summary="获取系统配置分类下的所有显示标签信息")
|
||||
async def get_settings_classifys(classify: str, db: AsyncSession = Depends(db_getter)):
|
||||
return SuccessResponse(await crud.SettingsTabDal(db).get_classify_tab_values([classify]))
|
||||
@app.get("/settings/base/config/", summary="获取系统基础配置", description="每次进入系统中时使用")
|
||||
async def get_setting_base_config(db: AsyncSession = Depends(db_getter)):
|
||||
return SuccessResponse(await crud.SettingsDal(db).get_base_config())
|
||||
|
||||
|
||||
@app.get("/settings/config/value/", summary="根据config_key获取到指定value")
|
||||
async def get_settings_config_value(config_key: str, db: AsyncSession = Depends(db_getter)):
|
||||
return SuccessResponse((await crud.SettingsDal(db).get_data(config_key=config_key)).config_value)
|
||||
@app.get("/settings/privacy/", summary="获取隐私协议")
|
||||
async def get_settings_privacy(auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_privacy")).config_value)
|
||||
|
||||
|
||||
@app.get("/settings/agreement/", summary="获取用户协议")
|
||||
async def get_settings_agreement(auth: Auth = Depends(FullAdminAuth())):
|
||||
return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_agreement")).config_value)
|
||||
|
@ -7,7 +7,8 @@
|
||||
# @desc : 简要说明
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
||||
from apps.vadmin.auth.utils.current import AllUserAuth
|
||||
from apps.vadmin.auth.utils.validation.auth import Auth
|
||||
from utils.response import SuccessResponse
|
||||
import datetime
|
||||
from apps.vadmin.record.crud import LoginRecordDal
|
||||
@ -19,7 +20,7 @@ app = APIRouter()
|
||||
# 工作区管理
|
||||
###########################################################
|
||||
@app.get("/total/", summary="获取统计")
|
||||
async def get_total(auth: Auth = Depends(login_auth)):
|
||||
async def get_total(auth: Auth = Depends(AllUserAuth())):
|
||||
data = {
|
||||
"project": 40,
|
||||
"access": await LoginRecordDal(auth.db).get_count(),
|
||||
|
@ -10,9 +10,12 @@
|
||||
# sqlalchemy 增删改操作:https://www.osgeo.cn/sqlalchemy/tutorial/orm_data_manipulation.html#updating-orm-objects
|
||||
# SQLAlchemy lazy load和eager load: https://www.jianshu.com/p/dfad7c08c57a
|
||||
# Mysql中内连接,左连接和右连接的区别总结:https://www.cnblogs.com/restartyang/articles/9080993.html
|
||||
# SQLAlchemy join 内连接
|
||||
# SQLAlchemy INNER JOIN 内连接
|
||||
# selectinload 官方文档:
|
||||
# https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.selectinload
|
||||
# SQLAlchemy LEFT OUTER JOIN 左连接
|
||||
# joinedload 官方文档:
|
||||
# https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.joinedload
|
||||
|
||||
import datetime
|
||||
from typing import List
|
||||
@ -21,11 +24,11 @@ from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import func, delete, update, or_
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from starlette import status
|
||||
from core.logger import logger
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
from typing import Any
|
||||
from sqlalchemy.engine.result import ScalarResult
|
||||
|
||||
|
||||
class DalBase:
|
||||
@ -49,22 +52,22 @@ class DalBase:
|
||||
"""
|
||||
获取单个数据,默认使用 ID 查询,否则使用关键词查询
|
||||
|
||||
@param data_id: 数据 ID
|
||||
@param v_join_query: 外键字段查询,内连接
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param v_schema: 指定使用的序列化对象
|
||||
@param v_order: 排序,默认正序,为 desc 是倒叙
|
||||
@param v_return_none: 是否返回空 None,否认 抛出异常,默认抛出异常
|
||||
@param kwargs: 查询参数
|
||||
:param data_id: 数据 ID
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param v_join_query: 外键字段查询,内连接
|
||||
:param v_order: 排序,默认正序,为 desc 是倒叙
|
||||
:param v_return_none: 是否返回空 None,否认 抛出异常,默认抛出异常
|
||||
:param v_schema: 指定使用的序列化对象
|
||||
:param kwargs: 查询参数
|
||||
"""
|
||||
sql = select(self.model).where(self.model.delete_datetime.is_(None))
|
||||
sql = select(self.model).where(self.model.is_delete == False)
|
||||
if data_id:
|
||||
sql = sql.where(self.model.id == data_id)
|
||||
sql = self.add_filter_condition(sql, v_join_query, v_options, **kwargs)
|
||||
if v_order and (v_order == "desc" or v_order == "descending"):
|
||||
sql = sql.order_by(self.model.create_datetime.desc())
|
||||
queryset = await self.db.execute(sql)
|
||||
data = queryset.scalars().first()
|
||||
data = queryset.scalars().unique().first()
|
||||
if not data and v_return_none:
|
||||
return None
|
||||
if data and v_schema:
|
||||
@ -88,19 +91,19 @@ class DalBase:
|
||||
):
|
||||
"""
|
||||
获取数据列表
|
||||
@param page: 页码
|
||||
@param limit: 当前页数据量
|
||||
@param v_join_query: 外键字段查询
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param v_schema: 指定使用的序列化对象
|
||||
@param v_order: 排序,默认正序,为 desc 是倒叙
|
||||
@param v_order_field: 排序字段
|
||||
@param v_return_objs: 是否返回对象
|
||||
@param v_start_sql: 初始 sql
|
||||
@param kwargs: 查询参数
|
||||
:param page: 页码
|
||||
:param limit: 当前页数据量
|
||||
:param v_join_query: 外键字段查询
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param v_schema: 指定使用的序列化对象
|
||||
:param v_order: 排序,默认正序,为 desc 是倒叙
|
||||
:param v_order_field: 排序字段
|
||||
:param v_return_objs: 是否返回对象
|
||||
:param v_start_sql: 初始 sql
|
||||
:param kwargs: 查询参数
|
||||
"""
|
||||
if not isinstance(v_start_sql, Select):
|
||||
v_start_sql = select(self.model).where(self.model.delete_datetime.is_(None))
|
||||
v_start_sql = select(self.model).where(self.model.is_delete == False)
|
||||
sql = self.add_filter_condition(v_start_sql, v_join_query, v_options, **kwargs)
|
||||
if v_order_field and (v_order == "desc" or v_order == "descending"):
|
||||
sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc())
|
||||
@ -112,19 +115,19 @@ class DalBase:
|
||||
sql = sql.offset((page - 1) * limit).limit(limit)
|
||||
queryset = await self.db.execute(sql)
|
||||
if v_return_objs:
|
||||
return queryset.scalars().all()
|
||||
return queryset.scalars().unique().all()
|
||||
if v_schema:
|
||||
return [v_schema.from_orm(i).dict() for i in queryset.scalars().all()]
|
||||
return [self.out_dict(i) for i in queryset.scalars().all()]
|
||||
return [v_schema.from_orm(i).dict() for i in queryset.scalars().unique().all()]
|
||||
return [self.out_dict(i) for i in queryset.scalars().unique().all()]
|
||||
|
||||
async def get_count(self, v_join_query: dict = None, v_options: list = None, **kwargs):
|
||||
"""
|
||||
获取数据总数
|
||||
@param v_join_query: 外键字段查询
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param kwargs: 查询参数
|
||||
:param v_join_query: 外键字段查询
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param kwargs: 查询参数
|
||||
"""
|
||||
sql = select(func.count(self.model.id).label('total')).where(self.model.delete_datetime.is_(None))
|
||||
sql = select(func.count(self.model.id).label('total')).where(self.model.is_delete == False)
|
||||
sql = self.add_filter_condition(sql, v_join_query, v_options, **kwargs)
|
||||
queryset = await self.db.execute(sql)
|
||||
return queryset.one()['total']
|
||||
@ -132,10 +135,10 @@ class DalBase:
|
||||
async def create_data(self, data, v_options: list = None, v_return_obj: bool = False, v_schema: Any = None):
|
||||
"""
|
||||
创建数据
|
||||
@param data: 创建数据
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param v_schema: ,指定使用的序列化对象
|
||||
@param v_return_obj: ,是否返回对象
|
||||
:param data: 创建数据
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param v_schema: ,指定使用的序列化对象
|
||||
:param v_return_obj: ,是否返回对象
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
obj = self.model(**data)
|
||||
@ -160,11 +163,11 @@ class DalBase:
|
||||
):
|
||||
"""
|
||||
更新单个数据
|
||||
@param data_id: 修改行数据的 ID
|
||||
@param data: 数据内容
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param v_return_obj: ,是否返回对象
|
||||
@param v_schema: ,指定使用的序列化对象
|
||||
:param data_id: 修改行数据的 ID
|
||||
:param data: 数据内容
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param v_return_obj: ,是否返回对象
|
||||
:param v_schema: ,指定使用的序列化对象
|
||||
"""
|
||||
obj = await self.get_data(data_id, v_options=v_options)
|
||||
obj_dict = jsonable_encoder(data)
|
||||
@ -177,17 +180,18 @@ class DalBase:
|
||||
return v_schema.from_orm(obj).dict()
|
||||
return self.out_dict(obj)
|
||||
|
||||
async def delete_datas(self, ids: List[int], soft: bool = False):
|
||||
async def delete_datas(self, ids: List[int], v_soft: bool = False, **kwargs):
|
||||
"""
|
||||
删除多条数据
|
||||
@param ids: 数据集
|
||||
@param soft: 是否执行软删除
|
||||
:param ids: 数据集
|
||||
:param v_soft: 是否执行软删除
|
||||
:param kwargs: 其他更新字段
|
||||
"""
|
||||
if soft:
|
||||
if v_soft:
|
||||
await self.db.execute(
|
||||
update(self.model)
|
||||
.where(self.model.id.in_(ids))
|
||||
.values(delete_datetime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
.values(delete_datetime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), is_delete=True, **kwargs)
|
||||
)
|
||||
else:
|
||||
await self.db.execute(delete(self.model).where(self.model.id.in_(ids)))
|
||||
@ -195,10 +199,10 @@ class DalBase:
|
||||
def add_filter_condition(self, sql: select, v_join_query: dict = None, v_options: list = None, **kwargs) -> select:
|
||||
"""
|
||||
添加过滤条件,以及内连接过滤条件
|
||||
@param sql:
|
||||
@param v_join_query: 外键字段查询,内连接
|
||||
@param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
@param kwargs: 关键词参数
|
||||
:param sql:
|
||||
:param v_join_query: 外键字段查询,内连接
|
||||
:param v_options: 指示应使用select在预加载中加载给定的属性。
|
||||
:param kwargs: 关键词参数
|
||||
"""
|
||||
if v_join_query and self.key_models:
|
||||
for key, value in v_join_query.items():
|
||||
@ -220,7 +224,7 @@ class DalBase:
|
||||
attr = getattr(self.model, field, None)
|
||||
sql = self.filter_condition(sql, attr, value)
|
||||
if v_options:
|
||||
sql = sql.options(*[selectinload(i) for i in v_options])
|
||||
sql = sql.options(*[load for load in v_options])
|
||||
return sql
|
||||
|
||||
@classmethod
|
||||
@ -231,7 +235,12 @@ class DalBase:
|
||||
if not attr:
|
||||
return sql
|
||||
if isinstance(value, tuple):
|
||||
if value[1]:
|
||||
if len(value) == 1:
|
||||
if value[0] == "None":
|
||||
sql = sql.where(attr.is_(None))
|
||||
elif value[0] == "not None":
|
||||
sql = sql.where(attr.isnot(None))
|
||||
elif len(value) == 2 and value[1] is not None:
|
||||
if value[0] == "date":
|
||||
# 根据日期查询, 关键函数是:func.time_format和func.date_format
|
||||
sql = sql.where(func.date_format(attr, "%Y-%m-%d") == value[1])
|
||||
@ -245,6 +254,10 @@ class DalBase:
|
||||
sql = sql.where(attr.between(value[1][0], value[1][1]))
|
||||
elif value[0] == "month":
|
||||
sql = sql.where(func.date_format(attr, "%Y-%m") == value[1])
|
||||
elif value[0] == "!=":
|
||||
sql = sql.where(attr != value[1])
|
||||
elif value[0] == ">":
|
||||
sql = sql.where(attr > value[1])
|
||||
else:
|
||||
sql = sql.where(attr == value)
|
||||
return sql
|
||||
@ -262,7 +275,7 @@ class DalBase:
|
||||
def out_dict(self, data: Any):
|
||||
"""
|
||||
序列化
|
||||
@param data:
|
||||
@return:
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
return self.schema.from_orm(data).dict()
|
||||
|
@ -28,9 +28,9 @@ def create_async_engine_session(database_url: str, database_type: str = "mysql")
|
||||
pool_timeout=20, # 池中没有连接最多等待的时间,否则报错
|
||||
pool_recycle=-1 # 多久之后对线程池中的线程进行一次连接的回收(重置)
|
||||
|
||||
@param database_type: 数据库类型
|
||||
@param database_url: 数据库地址
|
||||
@return:
|
||||
:param database_type: 数据库类型
|
||||
:param database_url: 数据库地址
|
||||
:return:
|
||||
"""
|
||||
engine = create_async_engine(
|
||||
database_url
|
||||
|
27
kinit-api/core/enum.py
Normal file
27
kinit-api/core/enum.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2023/02/12 22:18
|
||||
# @File : enum.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 增加枚举类方法
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SuperEnum(Enum):
|
||||
|
||||
@classmethod
|
||||
def to_dict(cls):
|
||||
"""Returns a dictionary representation of the enum."""
|
||||
return {e.name: e.value for e in cls}
|
||||
|
||||
@classmethod
|
||||
def keys(cls):
|
||||
"""Returns a list of all the enum keys."""
|
||||
return cls._member_names_
|
||||
|
||||
@classmethod
|
||||
def values(cls):
|
||||
"""Returns a list of all the enum values."""
|
||||
return list(cls._value2member_map_.keys())
|
@ -11,7 +11,7 @@ from fastapi import FastAPI
|
||||
from aioredis import from_url
|
||||
from application.settings import REDIS_DB_URL, MONGO_DB_URL, MONGO_DB_NAME
|
||||
from core.mongo import db
|
||||
from utils.cache import cache_aliyun_settings
|
||||
from utils.cache import Cache
|
||||
|
||||
|
||||
def register_redis(app: FastAPI) -> None:
|
||||
@ -21,25 +21,25 @@ def register_redis(app: FastAPI) -> None:
|
||||
博客:https://blog.csdn.net/wgPython/article/details/107668521
|
||||
博客:https://www.cnblogs.com/emunshe/p/15761597.html
|
||||
官网:https://aioredis.readthedocs.io/en/latest/getting-started/
|
||||
@param app:
|
||||
@return:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@app.on_event('startup')
|
||||
async def startup_event():
|
||||
"""
|
||||
获取链接
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
print("Connecting to Redis")
|
||||
app.state.redis = from_url(REDIS_DB_URL, decode_responses=True, health_check_interval=1)
|
||||
await cache_aliyun_settings(app.state.redis)
|
||||
await Cache(app.state.redis).cache_tab_names()
|
||||
|
||||
@app.on_event('shutdown')
|
||||
async def shutdown_event():
|
||||
"""
|
||||
关闭
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
print("Redis connection closed")
|
||||
await app.state.redis.close()
|
||||
@ -52,15 +52,15 @@ def register_mongo(app: FastAPI) -> None:
|
||||
博客:https://www.cnblogs.com/aduner/p/13532504.html
|
||||
mongodb 官网:https://www.mongodb.com/docs/drivers/motor/
|
||||
motor 文档:https://motor.readthedocs.io/en/stable/
|
||||
@param app:
|
||||
@return:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@app.on_event('startup')
|
||||
async def startup_event():
|
||||
"""
|
||||
获取 mongodb 连接
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
await db.connect_to_database(path=MONGO_DB_URL, db_name=MONGO_DB_NAME)
|
||||
|
||||
@ -68,6 +68,6 @@ def register_mongo(app: FastAPI) -> None:
|
||||
async def shutdown_event():
|
||||
"""
|
||||
关闭
|
||||
@return:
|
||||
:return:
|
||||
"""
|
||||
await db.close_database_connection()
|
||||
|
@ -68,13 +68,15 @@ def register_exception(app: FastAPI):
|
||||
msg = f"类型错误,提交参数应该为列表!"
|
||||
elif msg == "value is not a valid int":
|
||||
msg = f"类型错误,提交参数应该为整数!"
|
||||
elif msg == "value could not be parsed to a boolean":
|
||||
msg = f"类型错误,提交参数应该为布尔值!"
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content=jsonable_encoder(
|
||||
{
|
||||
"message": msg
|
||||
, "body": exc.body
|
||||
, "code": status.HTTP_400_BAD_REQUEST
|
||||
"message": msg,
|
||||
"body": exc.body,
|
||||
"code": status.HTTP_400_BAD_REQUEST
|
||||
}
|
||||
),
|
||||
)
|
||||
@ -91,8 +93,8 @@ def register_exception(app: FastAPI):
|
||||
status_code=200,
|
||||
content=jsonable_encoder(
|
||||
{
|
||||
"message": exc.__str__()
|
||||
, "code": status.HTTP_400_BAD_REQUEST
|
||||
"message": exc.__str__(),
|
||||
"code": status.HTTP_400_BAD_REQUEST
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@ -40,8 +40,8 @@ def write_request_log(request: Request, response: Response):
|
||||
def register_request_log_middleware(app: FastAPI):
|
||||
"""
|
||||
记录请求日志中间件
|
||||
@param app:
|
||||
@return:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@app.middleware("http")
|
||||
@ -58,8 +58,8 @@ def register_operation_record_middleware(app: FastAPI):
|
||||
"""
|
||||
操作记录中间件
|
||||
用于将使用认证的操作全部记录到 mongodb 数据库中
|
||||
@param app:
|
||||
@return:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@app.middleware("http")
|
||||
@ -121,8 +121,8 @@ def register_operation_record_middleware(app: FastAPI):
|
||||
def register_demo_env_middleware(app: FastAPI):
|
||||
"""
|
||||
演示环境中间件
|
||||
@param app:
|
||||
@return:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@app.middleware("http")
|
||||
|
@ -16,8 +16,8 @@ import re
|
||||
def vali_telephone(value: str) -> str:
|
||||
"""
|
||||
手机号验证器
|
||||
@param value: 手机号
|
||||
@return: 手机号
|
||||
:param value: 手机号
|
||||
:return: 手机号
|
||||
"""
|
||||
if not value or len(value) != 11 or not value.isdigit():
|
||||
raise ValueError("请输入正确手机号")
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
|
||||
from core.database import Model
|
||||
from sqlalchemy import Column, DateTime, Integer, func
|
||||
from sqlalchemy import Column, DateTime, Integer, func, Boolean
|
||||
|
||||
|
||||
# 使用命令:alembic init alembic 初始化迁移数据库环境
|
||||
@ -18,7 +18,8 @@ class BaseModel(Model):
|
||||
"""
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(Integer, primary_key=True, unique=True, comment='主键ID', index=True)
|
||||
id = Column(Integer, primary_key=True, unique=True, comment='主键ID', index=True, nullable=False)
|
||||
create_datetime = Column(DateTime, server_default=func.now(), comment='创建时间')
|
||||
update_datetime = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment='更新时间')
|
||||
delete_datetime = Column(DateTime, nullable=True, comment='删除时间')
|
||||
delete_datetime = Column(DateTime, nullable=True, comment='删除时间')
|
||||
is_delete = Column(Boolean, default=False, comment="是否软删除")
|
||||
|
@ -38,8 +38,8 @@ def init_app():
|
||||
openapi_url:配置接口文件json数据文件路由地址,如果禁用则为None,默认为/openapi.json
|
||||
"""
|
||||
app = FastAPI(
|
||||
title="KInit",
|
||||
description="本项目基于Fastapi与Vue3+Typescript+Vite+element-plus的基础项目 前端基于vue-element-plus-admin框架开发",
|
||||
title="Kinit",
|
||||
description="本项目基于Fastapi与Vue3+Typescript+Vite4+element-plus的基础项目 前端基于vue-element-plus-admin框架开发",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
@ -92,7 +92,7 @@ def init(env: Environment = Environment.pro):
|
||||
"""
|
||||
初始化数据
|
||||
|
||||
@params name: 数据库环境
|
||||
:params name: 数据库环境
|
||||
"""
|
||||
print("开始初始化数据")
|
||||
data = InitializeData()
|
||||
@ -104,7 +104,7 @@ def migrate(env: Environment = Environment.pro):
|
||||
"""
|
||||
将模型迁移到数据库,更新数据库表结构
|
||||
|
||||
@params name: 数据库环境
|
||||
:params name: 数据库环境
|
||||
"""
|
||||
print("开始更新数据库表")
|
||||
InitializeData().migrate_model(env)
|
||||
@ -115,7 +115,7 @@ def create_app(path: str):
|
||||
"""
|
||||
自动创建初始化 APP 结构
|
||||
|
||||
@params path: app 路径,根目录为apps,填写apps后面路径即可,例子:vadmin/auth
|
||||
:params path: app 路径,根目录为apps,填写apps后面路径即可,例子:vadmin/auth
|
||||
"""
|
||||
print(f"开始创建并初始化 {path} APP")
|
||||
app = CreateApp(path)
|
||||
|
Binary file not shown.
53
kinit-api/utils/aes_crypto.py
Normal file
53
kinit-api/utils/aes_crypto.py
Normal file
@ -0,0 +1,53 @@
|
||||
import base64
|
||||
from Crypto.Cipher import AES # 安装:pip install pycryptodome
|
||||
|
||||
|
||||
# 密钥(key), 密斯偏移量(iv) CBC模式加密
|
||||
# base64 详解:https://cloud.tencent.com/developer/article/1099008
|
||||
|
||||
_key = '0CoJUm6Qywm6ts68' # 自己密钥
|
||||
|
||||
|
||||
def aes_encrypt(data: str):
|
||||
"""
|
||||
加密
|
||||
"""
|
||||
vi = '0102030405060708'
|
||||
pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
|
||||
data = pad(data)
|
||||
# 字符串补位
|
||||
cipher = AES.new(_key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
|
||||
encrypted_bytes = cipher.encrypt(data.encode('utf8'))
|
||||
# 加密后得到的是bytes类型的数据
|
||||
encode_strs = base64.urlsafe_b64encode(encrypted_bytes)
|
||||
# 使用Base64进行编码,返回byte字符串
|
||||
# 对byte字符串按utf-8进行解码
|
||||
return encode_strs.decode('utf8')
|
||||
|
||||
|
||||
def aes_decrypt(data):
|
||||
"""
|
||||
解密
|
||||
"""
|
||||
vi = '0102030405060708'
|
||||
data = data.encode('utf8')
|
||||
encode_bytes = base64.urlsafe_b64decode(data)
|
||||
# 将加密数据转换位bytes类型数据
|
||||
cipher = AES.new(_key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
|
||||
text_decrypted = cipher.decrypt(encode_bytes)
|
||||
unpad = lambda s: s[0:-s[-1]]
|
||||
text_decrypted = unpad(text_decrypted)
|
||||
# 补位
|
||||
text_decrypted = text_decrypted.decode('utf8')
|
||||
return text_decrypted
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_data = '16658273438153332588-95YEUPJR' # 需要加密的内容
|
||||
|
||||
enctext = aes_encrypt(_data)
|
||||
print(enctext)
|
||||
|
||||
# enctext = "Wzll1oiVs9UKAySY1-xSy_CbrZmelVwyqu8P0CZTrrc="
|
||||
# _text_decrypted = aes_decrypt(_key, enctext)
|
||||
# print(_text_decrypted)
|
@ -19,7 +19,6 @@ Python 3
|
||||
pip install alibabacloud_tea_openapi
|
||||
pip install alibabacloud_dysmsapi20170525
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from enum import Enum, unique
|
||||
@ -31,7 +30,7 @@ from alibabacloud_tea_util import models as util_models
|
||||
from core.logger import logger
|
||||
import datetime
|
||||
from aioredis.client import Redis
|
||||
from utils.cache import cache_aliyun_settings
|
||||
from utils.cache import Cache
|
||||
from utils import status
|
||||
|
||||
|
||||
@ -52,28 +51,20 @@ class AliyunSMS:
|
||||
self.code = None
|
||||
self.scene = None
|
||||
|
||||
async def __get_settings(self, number: int = 3):
|
||||
async def __get_settings(self, retry: int = 3):
|
||||
"""
|
||||
获取配置信息
|
||||
"""
|
||||
aliyun_sms = await self.rd.get("aliyun_sms")
|
||||
if not aliyun_sms and number > 0:
|
||||
logger.error(f"未从Redis中获取到短信配置信息,正在重新更新配置信息,重试次数:{number}")
|
||||
await cache_aliyun_settings(self.rd)
|
||||
await self.__get_settings(number - 1)
|
||||
elif not aliyun_sms and number:
|
||||
raise CustomException("获取短信配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
|
||||
aliyun_sms = await Cache(self.rd).get_tab_name("aliyun_sms", retry)
|
||||
self.access_key = aliyun_sms.get("sms_access_key")
|
||||
self.access_key_secret = aliyun_sms.get("sms_access_key_secret")
|
||||
self.send_interval = int(aliyun_sms.get("sms_send_interval"))
|
||||
self.valid_time = int(aliyun_sms.get("sms_valid_time"))
|
||||
if self.scene == self.Scene.login:
|
||||
self.sign_name = aliyun_sms.get("sms_sign_name_1")
|
||||
else:
|
||||
aliyun_sms = json.loads(aliyun_sms)
|
||||
self.access_key = aliyun_sms.get("sms_access_key")
|
||||
self.access_key_secret = aliyun_sms.get("sms_access_key_secret")
|
||||
self.send_interval = int(aliyun_sms.get("sms_send_interval"))
|
||||
self.valid_time = int(aliyun_sms.get("sms_valid_time"))
|
||||
if self.scene == self.Scene.login:
|
||||
self.sign_name = aliyun_sms.get("sms_sign_name_1")
|
||||
else:
|
||||
self.sign_name = aliyun_sms.get("sms_sign_name_2")
|
||||
self.template_code = aliyun_sms.get(self.scene.value)
|
||||
self.sign_name = aliyun_sms.get("sms_sign_name_2")
|
||||
self.template_code = aliyun_sms.get(self.scene.value)
|
||||
|
||||
async def main_async(self, scene: Scene, **kwargs) -> bool:
|
||||
"""
|
||||
@ -152,8 +143,8 @@ class AliyunSMS:
|
||||
随机获取短信验证码
|
||||
短信验证码只支持数字,不支持字母及其他符号
|
||||
|
||||
@param length: 验证码长度
|
||||
@param blend: 是否 字母+数字 混合
|
||||
:param length: 验证码长度
|
||||
:param blend: 是否 字母+数字 混合
|
||||
"""
|
||||
code = "" # 创建字符串变量,存储生成的验证码
|
||||
for i in range(length): # 通过for循环控制验证码位数
|
||||
@ -186,10 +177,10 @@ class AliyunSMS:
|
||||
) -> Dysmsapi20170525Client:
|
||||
"""
|
||||
使用AK&SK初始化账号Client
|
||||
@param access_key_id:
|
||||
@param access_key_secret:
|
||||
@return: Client
|
||||
@throws Exception
|
||||
:param access_key_id:
|
||||
:param access_key_secret:
|
||||
:return: Client
|
||||
:throws Exception
|
||||
"""
|
||||
config = open_api_models.Config(
|
||||
# 您的 AccessKey ID,
|
||||
|
@ -4,22 +4,52 @@
|
||||
# @Creaet Time : 2022/3/21 11:03
|
||||
# @File : cache.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 保存缓存
|
||||
|
||||
# @desc : 缓存
|
||||
|
||||
from typing import List
|
||||
from core import logger
|
||||
from core.database import db_getter
|
||||
from apps.vadmin.system.crud import SettingsTabDal
|
||||
import json
|
||||
from aioredis.client import Redis
|
||||
from core.exception import CustomException
|
||||
from utils import status
|
||||
|
||||
|
||||
async def cache_aliyun_settings(rd: Redis):
|
||||
"""
|
||||
缓存阿里云配置信息
|
||||
"""
|
||||
async_session = db_getter()
|
||||
session = await async_session.__anext__()
|
||||
datas = await SettingsTabDal(session).get_classify_tab_values(["aliyun"], hidden=None)
|
||||
assert isinstance(rd, Redis)
|
||||
for k, v in datas.items():
|
||||
await rd.client().set(k, json.dumps(v))
|
||||
class Cache:
|
||||
|
||||
DEFAULT_TAB_NAMES = ["wx_server", "aliyun_sms", "aliyun_oss"]
|
||||
|
||||
def __init__(self, rd: Redis):
|
||||
self.rd = rd
|
||||
|
||||
async def cache_tab_names(self, tab_names: List[str] = None):
|
||||
"""
|
||||
缓存系统配置
|
||||
如果手动修改了mysql数据库中的配置
|
||||
那么需要在redis中将对应的tab_name删除
|
||||
"""
|
||||
async_session = db_getter()
|
||||
session = await async_session.__anext__()
|
||||
if tab_names:
|
||||
datas = await SettingsTabDal(session).get_tab_name_values(tab_names, hidden=None)
|
||||
else:
|
||||
datas = await SettingsTabDal(session).get_tab_name_values(self.DEFAULT_TAB_NAMES, hidden=None)
|
||||
for k, v in datas.items():
|
||||
await self.rd.client().set(k, json.dumps(v))
|
||||
|
||||
async def get_tab_name(self, tab_name: str, retry: int = 3):
|
||||
"""
|
||||
获取系统配置
|
||||
:params tab_name: 配置表标签名称
|
||||
:params retry: 重试次数
|
||||
"""
|
||||
result = await self.rd.get(tab_name)
|
||||
if not result and retry > 0:
|
||||
logger.error(f"未从Redis中获取到{tab_name}配置信息,正在重新更新配置信息,重试次数:{retry}")
|
||||
await self.cache_tab_names([tab_name])
|
||||
await self.get_tab_name(tab_name, retry - 1)
|
||||
elif not result and retry == 0:
|
||||
raise CustomException(f"获取{tab_name}配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
|
||||
else:
|
||||
return json.loads(result)
|
||||
|
@ -33,9 +33,9 @@ class ExcelManage:
|
||||
"""
|
||||
初始化 excel 文件
|
||||
|
||||
@param file: 文件名称或者对象
|
||||
@param read_only: 是否只读,优化读取速度
|
||||
@param data_only: 是否加载文件对象
|
||||
:param file: 文件名称或者对象
|
||||
:param read_only: 是否只读,优化读取速度
|
||||
:param data_only: 是否加载文件对象
|
||||
"""
|
||||
# 加载excel文件,获取表单
|
||||
self.wb = load_workbook(file, read_only=read_only, data_only=data_only)
|
||||
@ -44,7 +44,7 @@ class ExcelManage:
|
||||
"""
|
||||
初始化 excel 文件
|
||||
|
||||
@param sheet_name: 表单名称,为空则默认第一个
|
||||
:param sheet_name: 表单名称,为空则默认第一个
|
||||
"""
|
||||
# 加载excel文件,获取表单
|
||||
if not self.wb:
|
||||
@ -57,7 +57,7 @@ class ExcelManage:
|
||||
def get_sheets(self) -> list:
|
||||
"""
|
||||
读取所有工作区名称
|
||||
@return: 一维数组
|
||||
:return: 一维数组
|
||||
"""
|
||||
return self.wb.sheetnames
|
||||
|
||||
@ -65,7 +65,7 @@ class ExcelManage:
|
||||
"""
|
||||
创建 excel 文件
|
||||
|
||||
@param sheet_name: 表单名称,为空则默认第一个
|
||||
:param sheet_name: 表单名称,为空则默认第一个
|
||||
"""
|
||||
# 加载excel文件,获取表单
|
||||
self.wb = Workbook()
|
||||
@ -77,7 +77,7 @@ class ExcelManage:
|
||||
"""
|
||||
读取指定表单所有数据
|
||||
|
||||
@return: 二维数组
|
||||
:return: 二维数组
|
||||
"""
|
||||
rows = self.sheet.iter_rows(min_row=min_row, min_col=min_col, max_row=max_row, max_col=max_col)
|
||||
result = []
|
||||
@ -93,10 +93,10 @@ class ExcelManage:
|
||||
"""
|
||||
读取指定表单的表头(第一行数据)
|
||||
|
||||
@param row: 指定行
|
||||
@param col: 最大列
|
||||
@param asterisk: 是否去除 * 号
|
||||
@return: 一维数组
|
||||
:param row: 指定行
|
||||
:param col: 最大列
|
||||
:param asterisk: 是否去除 * 号
|
||||
:return: 一维数组
|
||||
"""
|
||||
rows = self.sheet.iter_rows(min_row=row, max_col=col)
|
||||
result = []
|
||||
@ -111,8 +111,8 @@ class ExcelManage:
|
||||
"""
|
||||
写入 excel文件
|
||||
|
||||
@param rows: 行数据集
|
||||
@param header: 表头
|
||||
:param rows: 行数据集
|
||||
:param header: 表头
|
||||
"""
|
||||
if header:
|
||||
self.sheet.append(header)
|
||||
@ -152,8 +152,8 @@ class ExcelManage:
|
||||
"""
|
||||
设置行样式
|
||||
|
||||
@param row: 行
|
||||
@param max_column: 最大列
|
||||
:param row: 行
|
||||
:param max_column: 最大列
|
||||
"""
|
||||
for index in range(0, max_column):
|
||||
# 设置单元格对齐方式
|
||||
@ -165,8 +165,8 @@ class ExcelManage:
|
||||
"""
|
||||
设置行样式
|
||||
|
||||
@param row: 行
|
||||
@param columns: 列数据
|
||||
:param row: 行
|
||||
:param columns: 列数据
|
||||
"""
|
||||
for index in columns.get("date_columns", []):
|
||||
self.sheet.cell(row=row, column=index).number_format = "yyyy/mm/dd h:mm:ss"
|
||||
|
@ -48,9 +48,9 @@ class WriteXlsx:
|
||||
def generate_template(self, headers: List[dict] = None, max_row: int = 101) -> None:
|
||||
"""
|
||||
生成模板
|
||||
@param headers: 表头
|
||||
@param max_row: 设置下拉列表至最大行
|
||||
@return: 文件链接地址
|
||||
:param headers: 表头
|
||||
:param max_row: 设置下拉列表至最大行
|
||||
:return: 文件链接地址
|
||||
"""
|
||||
self.create_excel()
|
||||
max_row = max_row + 100
|
||||
@ -80,8 +80,8 @@ class WriteXlsx:
|
||||
"""
|
||||
写入 excel文件
|
||||
|
||||
@param rows: 行数据集
|
||||
@param start_row: 开始行
|
||||
:param rows: 行数据集
|
||||
:param start_row: 开始行
|
||||
"""
|
||||
font_format = {
|
||||
'bold': False, # 字体加粗
|
||||
|
@ -49,10 +49,10 @@ class AliyunOSS(FileBase):
|
||||
"""
|
||||
上传图片
|
||||
|
||||
@param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。
|
||||
@param file: 文件对象
|
||||
@param compress: 是否压缩该文件
|
||||
@return: 上传后的文件oss链接
|
||||
:param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。
|
||||
:param file: 文件对象
|
||||
:param compress: 是否压缩该文件
|
||||
:return: 上传后的文件oss链接
|
||||
"""
|
||||
path = self.generate_path(path, file.filename)
|
||||
if compress:
|
||||
@ -76,9 +76,9 @@ class AliyunOSS(FileBase):
|
||||
"""
|
||||
上传文件
|
||||
|
||||
@param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。
|
||||
@param file: 文件对象
|
||||
@return: 上传后的文件oss链接
|
||||
:param path: path由包含文件后缀,不包含Bucket名称组成的Object完整路径,例如abc/efg/123.jpg。
|
||||
:param file: 文件对象
|
||||
:return: 上传后的文件oss链接
|
||||
"""
|
||||
path = self.generate_path(path, file.filename)
|
||||
file_data = await file.read()
|
||||
|
@ -38,7 +38,7 @@ class FileBase:
|
||||
"""
|
||||
验证文件是否符合格式
|
||||
|
||||
@params max_size: 文件最大值,单位 MB
|
||||
:params max_size: 文件最大值,单位 MB
|
||||
"""
|
||||
if max_size:
|
||||
size = len(await file.read()) / 1024 / 1024
|
||||
|
@ -69,8 +69,8 @@ class FileManage(FileBase):
|
||||
复制文件
|
||||
根目录为项目根目录,传过来的文件路径均为相对路径
|
||||
|
||||
@param src: 原始文件
|
||||
@param dst: 目标路径。绝对路径
|
||||
:param src: 原始文件
|
||||
:param dst: 目标路径。绝对路径
|
||||
"""
|
||||
if src[0] == "/":
|
||||
src = src.lstrip("/")
|
||||
|
@ -15,6 +15,7 @@ https://api.ip138.com/ip/?ip=58.16.180.3&datatype=jsonp&token=cc87f3c77747bccbaa
|
||||
|
||||
aiohttp 异步请求文档:https://docs.aiohttp.org/en/stable/client_quickstart.html
|
||||
"""
|
||||
from aiohttp import TCPConnector
|
||||
|
||||
from application.settings import IP_PARSE_TOKEN, IP_PARSE_ENABLE
|
||||
import aiohttp
|
||||
@ -39,7 +40,7 @@ class IPManage:
|
||||
|
||||
def __init__(self, ip: str):
|
||||
self.ip = ip
|
||||
self.url = f"http://api.ip138.com/ip/?ip={ip}&datatype=jsonp&token={IP_PARSE_TOKEN}"
|
||||
self.url = f"https://api.ip138.com/ip/?ip={ip}&datatype=jsonp&token={IP_PARSE_TOKEN}"
|
||||
|
||||
async def parse(self):
|
||||
"""
|
||||
@ -52,7 +53,7 @@ class IPManage:
|
||||
if not IP_PARSE_ENABLE:
|
||||
logger.warning("未开启IP地址数据解析,无法获取到IP所属地,请在application/config/production.py:IP_PARSE_ENABLE中开启!")
|
||||
return out
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(connector=TCPConnector(ssl=False)) as session:
|
||||
async with session.get(self.url) as resp:
|
||||
body = await resp.json()
|
||||
if body.get("ret") != 'ok':
|
||||
|
@ -7,11 +7,13 @@
|
||||
# @desc : 工具类
|
||||
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
from typing import List
|
||||
import string
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
def test_password(password: str) -> str | bool:
|
||||
def test_password(password: str) -> Union[str, bool]:
|
||||
"""
|
||||
检测密码强度
|
||||
"""
|
||||
@ -32,7 +34,7 @@ def test_password(password: str) -> str | bool:
|
||||
return '至少含数字/字母/字符2种组合,请重新输入。'
|
||||
|
||||
|
||||
def list_dict_find(options: List[dict], key: str, value: any) -> dict | None:
|
||||
def list_dict_find(options: List[dict], key: str, value: any) -> Union[dict, None]:
|
||||
"""
|
||||
字典列表查找
|
||||
"""
|
||||
@ -42,6 +44,37 @@ def list_dict_find(options: List[dict], key: str, value: any) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_time_interval(start_time: str, end_time: str, interval: int, time_format: str = "%H:%M:%S") -> List:
|
||||
"""
|
||||
获取时间间隔
|
||||
:param end_time: 结束时间
|
||||
:param start_time: 开始时间
|
||||
:param interval: 间隔时间(分)
|
||||
:param time_format: 字符串格式化,默认:%H:%M:%S
|
||||
"""
|
||||
if start_time.count(":") == 1:
|
||||
start_time = f"{start_time}:00"
|
||||
if end_time.count(":") == 1:
|
||||
end_time = f"{end_time}:00"
|
||||
start_time = datetime.datetime.strptime(start_time, "%H:%M:%S")
|
||||
end_time = datetime.datetime.strptime(end_time, "%H:%M:%S")
|
||||
time_range = []
|
||||
while end_time > start_time:
|
||||
time_range.append(start_time.strftime(time_format))
|
||||
start_time = start_time + datetime.timedelta(minutes=interval)
|
||||
return time_range
|
||||
|
||||
|
||||
def generate_string(length: int = 8) -> str:
|
||||
"""
|
||||
生成随机字符串
|
||||
:param length: 字符串长度
|
||||
"""
|
||||
return ''.join(random.sample(string.ascii_letters + string.digits, length))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# print(generate_invitation_code())
|
||||
print(int(datetime.datetime.now().timestamp()))
|
||||
# print(int(datetime.datetime.now().timestamp()))
|
||||
# print(datetime.datetime.today() + datetime.timedelta(days=7))
|
||||
print(generate_string())
|
||||
|
7
kinit-api/utils/wx/__init__.py
Normal file
7
kinit-api/utils/wx/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/3/15 20:18
|
||||
# @File : __init__.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
137
kinit-api/utils/wx/oauth.py
Normal file
137
kinit-api/utils/wx/oauth.py
Normal file
@ -0,0 +1,137 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2022/3/15 20:44
|
||||
# @File : oauth.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 简要说明
|
||||
|
||||
import json
|
||||
import requests
|
||||
from core.logger import logger
|
||||
from utils.cache import Cache
|
||||
from utils.wx.wx_access_token import WxAccessToken
|
||||
from aioredis import Redis
|
||||
|
||||
|
||||
class WXOAuth:
|
||||
|
||||
def __init__(self, rd: Redis, index: int = 0):
|
||||
"""
|
||||
初始化微信认证
|
||||
:param index: 选择小程序,0:微信服务端
|
||||
"""
|
||||
# 重试次数
|
||||
self.retry_count = 5
|
||||
self.appid = None
|
||||
self.secret = None
|
||||
self.rd = rd
|
||||
self.tab_name = None
|
||||
if index == 0:
|
||||
self.tab_name = "wx_server"
|
||||
|
||||
async def __get_settings(self, retry: int = 3):
|
||||
"""
|
||||
获取配置信息
|
||||
"""
|
||||
if not self.tab_name:
|
||||
logger.error(f"请选择认证的微信平台")
|
||||
wx_config = await Cache(self.rd).get_tab_name(self.tab_name, retry)
|
||||
self.appid = wx_config.get("wx_server_app_id")
|
||||
self.secret = wx_config.get("wx_server_app_secret")
|
||||
|
||||
async def get_code2session(self, code: str) -> dict:
|
||||
"""
|
||||
通过微信用户临时登录凭证 code 进行校验,获取用户openid,与 session 信息
|
||||
|
||||
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
|
||||
:param code: 登录时获取的 code
|
||||
:return: 正确:{'session_key': 'F8/5LZrdtINYLPEdUJgXXQ==', 'openid': 'okLlC5Kcv7DH2J99dz-Z2FwJeEeU'}
|
||||
:return: 报错:{'errcode': 40029, 'errmsg': 'invalid code, rid: 62308e5d-0b0b697e-1db652eb'}
|
||||
"""
|
||||
if not self.appid or not self.secret:
|
||||
await self.__get_settings()
|
||||
api = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
params = {
|
||||
"appid": self.appid,
|
||||
"secret": self.secret,
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
response = requests.get(url=api, params=params)
|
||||
result = response.json()
|
||||
if "openid" not in result:
|
||||
logger.error(f"微信校验失败:{result}, code:{code}")
|
||||
else:
|
||||
logger.info(f"微信校验成功:{result}, code:{code}")
|
||||
return result
|
||||
|
||||
async def get_phone_number(self, code: str):
|
||||
"""
|
||||
获取微信用户手机号
|
||||
|
||||
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html
|
||||
:param code: 动态令牌。可通过动态令牌换取用户手机号。
|
||||
:return: 成功:{'errcode': 0, 'errmsg': 'ok', 'phone_info': {'phoneNumber': '15093430559'
|
||||
, 'purePhoneNumber': '15093430559', 'countryCode': '86', 'watermark': {'timestamp': 1647355468, 'appid': 'wx069c452f9a733df1'}}}
|
||||
失败:{'errcode': 40001, 'errmsg': 'invalid credential, access_token is invalid or not latest rid: 62690257-2894b530-58c6fcf3'}
|
||||
"""
|
||||
if not self.appid or not self.secret:
|
||||
await self.__get_settings()
|
||||
api = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
at = WxAccessToken(self.appid, self.secret, self.rd)
|
||||
access_token = await at.get()
|
||||
if not access_token.get("status", False):
|
||||
result = {'errcode': 40001, 'errmsg': '获取微信令牌失败'}
|
||||
# print(result)
|
||||
logger.error(f"获取微信用户手机号失败:{result}")
|
||||
return result
|
||||
params = {
|
||||
"access_token": access_token.get("token"),
|
||||
}
|
||||
data = {
|
||||
"code": code,
|
||||
}
|
||||
response = requests.post(url=api, params=params, json=data)
|
||||
result = response.json()
|
||||
if result.get("errcode", 0) == 0:
|
||||
# print("获取微信用户手机号成功", result)
|
||||
logger.info(f"获取微信用户手机号成功:{result}, code:{code}")
|
||||
else:
|
||||
# print("获取微信用户手机号失败", result)
|
||||
logger.error(f"获取微信用户手机号失败:{result}, code:{code}")
|
||||
if result.get("errcode", 0) == 40001:
|
||||
await at.update()
|
||||
if self.retry_count > 0:
|
||||
logger.error(f"重试获取微信手机号,重试剩余次数, {self.retry_count}")
|
||||
self.retry_count -= 1
|
||||
return await self.get_phone_number(code)
|
||||
return result
|
||||
|
||||
async def parsing_phone_number(self, code: str):
|
||||
"""
|
||||
解析微信用户手机号
|
||||
:param code: 动态令牌。可通过动态令牌换取用户手机号。
|
||||
:return:
|
||||
"""
|
||||
result = await self.get_phone_number(code)
|
||||
if result.get("errcode") == 0:
|
||||
phone_info = result["phone_info"]
|
||||
assert isinstance(phone_info, dict)
|
||||
return phone_info["phoneNumber"]
|
||||
return None
|
||||
|
||||
async def parsing_openid(self, code: str):
|
||||
"""
|
||||
解析openid
|
||||
:param code: 动态令牌。可通过动态令牌换取用户手机号。
|
||||
:return: openid | None
|
||||
"""
|
||||
result = await self.get_code2session(code)
|
||||
if "openid" in result:
|
||||
return result["openid"]
|
||||
return None
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# WXOAuth().get_code2session("063mPDFa1v16PC0yqhGa1uQ86t4mPDFV")
|
61
kinit-api/utils/wx/wx_access_token.py
Normal file
61
kinit-api/utils/wx/wx_access_token.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @version : 1.0
|
||||
# @Creaet Time : 2021/11/27 18:37
|
||||
# @File : wx_access_token.py
|
||||
# @IDE : PyCharm
|
||||
# @desc : 获取小程序全局唯一后台接口调用凭据
|
||||
|
||||
import requests
|
||||
from aioredis import Redis
|
||||
from core.logger import logger
|
||||
|
||||
|
||||
class WxAccessToken:
|
||||
"""
|
||||
获取到的access_token存储在redis数据库中
|
||||
|
||||
获取小程序全局唯一后台接口调用凭据(access_token)。调用绝大多数后台接口时都需使用 access_token,开发者需要进行妥善保存。
|
||||
|
||||
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
|
||||
"""
|
||||
|
||||
def __init__(self, appid: str, secret: str, redis: Redis, grant_type: str = "client_credential", *args, **kwargs):
|
||||
self.__url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
self.__method = "get"
|
||||
self.appidKey = f"{appid}_access_token"
|
||||
self.redis = redis
|
||||
self.params = {
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
"grant_type": grant_type
|
||||
}
|
||||
|
||||
async def get(self) -> dict:
|
||||
"""
|
||||
获取小程序access_token
|
||||
"""
|
||||
token = await self.redis.get(self.appidKey)
|
||||
if not token:
|
||||
return await self.update()
|
||||
return {"status": True, "token": token}
|
||||
|
||||
async def update(self) -> dict:
|
||||
"""
|
||||
更新小程序access_token
|
||||
"""
|
||||
print("开始更新 access_token")
|
||||
method = getattr(requests, self.__method)
|
||||
response = method(url=self.__url, params=self.params)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", "0") != "0":
|
||||
print("获取access_token失败", result)
|
||||
logger.error(f"获取access_token失败:{result}")
|
||||
return {"status": False, "token": None}
|
||||
|
||||
print("成功获取到", result)
|
||||
await self.redis.set(self.appidKey, result.get("access_token"), ex=2000)
|
||||
logger.info(f"获取access_token成功:{result}")
|
||||
|
||||
return {"status": True, "token": result.get("access_token")}
|
@ -10,7 +10,7 @@
|
||||
// 初始化应用
|
||||
initApp() {
|
||||
// 初始化应用配置
|
||||
this.$store.dispatch('InitConfig')
|
||||
this.$store.dispatch('app/InitConfig')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
65
kinit-uni/common/mixins/auth.js
Normal file
65
kinit-uni/common/mixins/auth.js
Normal file
@ -0,0 +1,65 @@
|
||||
// 微信存储:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/storage.html
|
||||
// 微信登录
|
||||
// 微信小程序登录流程:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
|
||||
// 登录失败的原因可能是因为没将后台IP添加到白名单
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { setUserOpenid } from '@/common/request/api/login.js'
|
||||
import { toast } from '@/common/utils/common'
|
||||
|
||||
export const wxLoginMixins = {
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isUserOpenid',
|
||||
])
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onGetPhoneNumber(e) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 获取手机号官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
|
||||
if (e.detail.errMsg === "getPhoneNumber:fail user deny") {
|
||||
// 用户拒绝授权
|
||||
toast("已取消授权")
|
||||
reject("已取消授权")
|
||||
} else if (e.detail.errMsg === "getPhoneNumber:fail no permission") {
|
||||
// 微信公众平台未认证或未使用企业认证
|
||||
toast("微信公众平台未认证或未使用企业认证")
|
||||
reject("微信公众平台未认证或未使用企业认证")
|
||||
} else if (e.detail.errMsg === "getPhoneNumber:ok") {
|
||||
// code换取用户手机号。 每个code只能使用一次,code的有效期为5min
|
||||
this.$store.dispatch('auth/wxLogin', e.detail.code).then(res => {
|
||||
this.setOpenid();
|
||||
this.$store.dispatch('auth/GetInfo').then(result => {
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
toast("授权失败")
|
||||
reject("授权失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
setOpenid() {
|
||||
let self = this;
|
||||
// uniapp 官方文档:https://uniapp.dcloud.io/api/plugins/login.html#login
|
||||
if (self.isUserOpenid) { return; };
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: function (loginRes) {
|
||||
if (loginRes.code) {
|
||||
setUserOpenid(loginRes.code).then(res => {
|
||||
// console.log("更新openid成功", res)
|
||||
self.$store.commit("auth/SET_IS_USER_OPENID", true);
|
||||
})
|
||||
} else {
|
||||
console.log('登录失败!获取code失败!' + res.errMsg)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
46
kinit-uni/common/mixins/share.js
Normal file
46
kinit-uni/common/mixins/share.js
Normal file
@ -0,0 +1,46 @@
|
||||
export const wxShareMixins = {
|
||||
data() {
|
||||
return {
|
||||
share: {
|
||||
title: "",
|
||||
path: "",
|
||||
imageUrl: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad: function() {
|
||||
wx.showShareMenu({
|
||||
withShareTicket: true,
|
||||
menus: ["shareAppMessage", "shareTimeline"]
|
||||
})
|
||||
},
|
||||
onShareAppMessage(res) {
|
||||
let that = this;
|
||||
let imageUrl = that.share.imageUrl || '';
|
||||
if (res.from === 'button') {
|
||||
//这块需要传参,不然链接地址进去获取不到数据
|
||||
let path = `/` + that.$scope.route + `?item=` + that.$scope.options.item;
|
||||
return {
|
||||
title: '商品分享~',
|
||||
path: path,
|
||||
imageUrl: imageUrl
|
||||
};
|
||||
}
|
||||
if (res.from === 'menu') {
|
||||
return {
|
||||
title: that.share.title,
|
||||
path: that.share.path,
|
||||
imageUrl: that.share.imageUrl
|
||||
};
|
||||
}
|
||||
},
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: this.share.title,
|
||||
path: this.share.path,
|
||||
imageUrl: this.share.imageUrl
|
||||
};
|
||||
},
|
||||
methods: {}
|
||||
}
|
@ -1,17 +1,33 @@
|
||||
import request from '@/common/request/request.js'
|
||||
|
||||
// 登录方法
|
||||
export function login(telephone, password, method) {
|
||||
export function login(telephone, password) {
|
||||
const data = {
|
||||
telephone,
|
||||
password,
|
||||
method,
|
||||
method: '0',
|
||||
platform: '1'
|
||||
}
|
||||
return request.post(`/auth/login/`, data)
|
||||
return request.post(`/auth/login/`, data)
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
export function getInfo() {
|
||||
return request.get(`/vadmin/auth/user/current/info/`)
|
||||
return request.get(`/vadmin/auth/user/admin/current/info/`)
|
||||
}
|
||||
|
||||
// 更新用户openid
|
||||
export function setUserOpenid(code) {
|
||||
const params = {code}
|
||||
return request.put(`/vadmin/auth/users/wx/server/openid/`, {}, {params: params})
|
||||
}
|
||||
|
||||
// 使用微信一键登录
|
||||
export function wxCodeLogin(code) {
|
||||
const data = {
|
||||
code,
|
||||
method: '2',
|
||||
platform: '1'
|
||||
}
|
||||
return request.post(`/auth/wx/login/`, data)
|
||||
}
|
||||
|
16
kinit-uni/common/request/api/vadmin/help/issue.js
Normal file
16
kinit-uni/common/request/api/vadmin/help/issue.js
Normal file
@ -0,0 +1,16 @@
|
||||
import request from '@/common/request/request.js'
|
||||
|
||||
// 获取平台中的常见问题类别列表
|
||||
export function getIssueCategoryList() {
|
||||
return request.get(`/vadmin/help/issue/categorys/platform/1/`)
|
||||
}
|
||||
|
||||
// 获取问题详情
|
||||
export function getIssue(dataId) {
|
||||
return request.get(`/vadmin/help/issues/${dataId}/`)
|
||||
}
|
||||
|
||||
// 更新常见问题查看次数+1
|
||||
export function updateIssueAddViewNumber(dataId) {
|
||||
return request.get(`/vadmin/help/issues/add/view/number/${dataId}/`)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import request from '@/common/request/request'
|
||||
|
||||
// 获取系统配置分类
|
||||
export function getSystemSettingsClassifysApi(params) {
|
||||
return request.get(`/vadmin/system/settings/classifys/`, {params: params})
|
||||
// 获取系统基本配置
|
||||
export function getSystemBaseConfigApi() {
|
||||
return request.get(`/vadmin/system/settings/base/config/`)
|
||||
}
|
@ -49,7 +49,7 @@ http.interceptors.response.use(res => {
|
||||
} else if (code === 401) {
|
||||
showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => {
|
||||
if (res.confirm) {
|
||||
store.dispatch('LogOut')
|
||||
store.dispatch('auth/LogOut')
|
||||
}
|
||||
})
|
||||
return Promise.reject("error");
|
||||
|
@ -1,12 +1,13 @@
|
||||
const constant = {
|
||||
avatar: 'vuex_avatar',
|
||||
name: 'vuex_name',
|
||||
nickname: 'vuex_nickname',
|
||||
telephone: 'vuex_telephone',
|
||||
isUser: 'vuex_isUser',
|
||||
roles: 'vuex_roles',
|
||||
create_datetime: 'vuex_createDatetime',
|
||||
permissions: 'vuex_permissions'
|
||||
}
|
||||
|
||||
export default constant
|
||||
export const auth = {
|
||||
isUser: 'vuex_auth_isUser',
|
||||
isUserOpenid: 'vuex_auth_isUserOpenid',
|
||||
isResetPassword: 'vuex_auth_isResetPassword',
|
||||
name: 'vuex_auth_name',
|
||||
nickname: 'vuex_auth_nickname',
|
||||
gender: 'vuex_auth_gender',
|
||||
telephone: 'vuex_auth_telephone',
|
||||
avatar: 'vuex_auth_avatar',
|
||||
createDatetime: 'vuex_auth_createDatetime',
|
||||
roles: 'vuex_auth_roles',
|
||||
permissions: 'vuex_auth_permissions'
|
||||
}
|
||||
|
@ -6,21 +6,20 @@
|
||||
*/
|
||||
|
||||
|
||||
import { API_BASE_URL } from '@/common/setting/index'
|
||||
import { getToken, removeToken, getStorage } from '@/common/utils/cookies'
|
||||
import config from '@/config.js'
|
||||
import { getToken, removeToken } from '@/common/utils/cookies'
|
||||
|
||||
|
||||
// 单个文件上传
|
||||
export function uploadFile(api, file, data={}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: API_BASE_URL + api,
|
||||
url: config.baseUrl + api,
|
||||
filePath: file,
|
||||
name: 'file',
|
||||
timeout: 60000,
|
||||
formData: data,
|
||||
header: {
|
||||
ossign: getStorage("ossign"),
|
||||
Authorization: getToken()
|
||||
},
|
||||
success: (res) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user