版本升级

1. 新增:微信小程序端新增微信手机号登录功能(必须为企业认证小程序)
2. 新增:加入动态更新常见问题
3. 新增:新增小程序分享功能
4. 新增:小程序新增第一次登录需要修改密码
5. 新增:新增接口权限控制
6. 新增:用户新增is_staff用来判断是否为工作人员
7. 新增:软删除新增is_delete字段来判断,delete_datetime当前主要来记录时间
8. 更新:部分接口删除功能已更新,需要试用软删除的才会试用软删除
9. 更新:更新系统配置缓存功能
10. 更新:接口认证依赖项更新
11. 更新:获取系统基础配置信息与用户协议与隐私协议更新
12. 优化:优化接口与数据库操作
This commit is contained in:
ktianc 2023-02-27 17:28:27 +08:00
parent 7c1723c9d2
commit 7fbdcb3b0f
123 changed files with 3439 additions and 829 deletions

View File

@ -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
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/2.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/10.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/3.png)
![image-20221010214526082](https://gitee.com/ktianc/kinit/raw/master/images/6.jpg)

BIN
images/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

View File

@ -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 是一套开箱即用的中后台解决方案,可以作为新项目的启动模版。'
)
}
}

View File

@ -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
})
}

View 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}/` })
}

View 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
})
}

View File

@ -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/' })
}

View File

@ -63,7 +63,6 @@ const dialogStyle = computed(() => {
destroy-on-close
lock-scroll
draggable
align-center
:close-on-click-modal="false"
>
<template #header>

View File

@ -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

View File

@ -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 }

View File

@ -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)

View File

@ -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'))
}

View File

@ -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) =>

View File

@ -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()

View File

@ -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
},

View File

@ -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
}

View File

@ -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>

View File

@ -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

View File

@ -18,6 +18,7 @@ const props = defineProps({
const rules = reactive({
name: [required()],
is_active: [required()],
is_staff: [required()],
role_ids: [required()],
telephone: [required()]
})

View File

@ -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
}
]
}
}
])

View File

@ -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>

View 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>

View 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
}
]
}
}
])

View 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>

View File

@ -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>

View File

@ -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
}
]
}
}
])

View 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>

View File

@ -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>

View File

@ -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
}

View File

@ -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
}

View File

@ -36,7 +36,7 @@ export const columns = reactive<TableColumn[]>([
{
field: 'login_method',
label: '认证方式',
width: '120px',
width: '150px',
show: true,
span: 24
},

View File

@ -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
}
}
])

View File

@ -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>

View 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>

View File

@ -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/"
]

View File

@ -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": ["帮助中心管理"]},
]

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -7,4 +7,4 @@
# @desc : 简要说明
from .auth import Auth, AuthValidation
from .login import LoginValidation, LoginForm, LoginResult
from .login import LoginValidation, LoginForm, LoginResult, WXLoginForm

View File

@ -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()

View File

@ -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' # 登录平台0PC端管理系统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' # 登录平台0PC端管理系统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

View File

@ -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})

View 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 : 帮助中心

View 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)

View 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

View 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)

View 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

View 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)

View 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

View 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

View 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

View 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

View 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))

View File

@ -32,7 +32,7 @@ class LoginRecordDal(DalBase):
total: 20
}
@return: List[dict]
:return: List[dict]
"""
result = [{
"name": '北京',

View File

@ -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,

View File

@ -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())

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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(),

View File

@ -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()

View File

@ -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
View 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())

View File

@ -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()

View File

@ -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
}
),
)

View File

@ -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")

View File

@ -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("请输入正确手机号")

View File

@ -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="是否软删除")

View File

@ -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)

View 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)

View File

@ -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,

View File

@ -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)

View File

@ -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"

View File

@ -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, # 字体加粗

View File

@ -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()

View File

@ -38,7 +38,7 @@ class FileBase:
"""
验证文件是否符合格式
@params max_size: 文件最大值单位 MB
:params max_size: 文件最大值单位 MB
"""
if max_size:
size = len(await file.read()) / 1024 / 1024

View File

@ -69,8 +69,8 @@ class FileManage(FileBase):
复制文件
根目录为项目根目录传过来的文件路径均为相对路径
@param src: 原始文件
@param dst: 目标路径绝对路径
:param src: 原始文件
:param dst: 目标路径绝对路径
"""
if src[0] == "/":
src = src.lstrip("/")

View File

@ -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':

View File

@ -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())

View 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
View 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")

View 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")}

View File

@ -10,7 +10,7 @@
//
initApp() {
//
this.$store.dispatch('InitConfig')
this.$store.dispatch('app/InitConfig')
}
}
}

View 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)
}
}
});
}
}
}

View 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: {}
}

View File

@ -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)
}

View 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}/`)
}

View File

@ -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/`)
}

View File

@ -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");

View File

@ -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'
}

View File

@ -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