Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
李强
2024-06-28 21:04:46 +08:00
16 changed files with 178 additions and 125 deletions

View File

@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth from django.contrib import auth
from django.contrib.auth import login from django.contrib.auth import login
from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi from drf_yasg import openapi
@@ -83,11 +84,18 @@ class LoginSerializer(TokenObtainPairSerializer):
else: else:
self.image_code and self.image_code.delete() self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误") raise CustomValidationError("图片验证码错误")
try:
user = Users.objects.get(username=attrs['username']) user = Users.objects.get(
Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username']))
except Users.DoesNotExist:
raise CustomValidationError("您登录的账号不存在")
except Users.MultipleObjectsReturned:
raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性")
if not user.is_active: if not user.is_active:
raise CustomValidationError("账号已被锁定,联系管理员解锁") raise CustomValidationError("账号已被锁定,联系管理员解锁")
try: try:
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
attrs['username'] = user.username
data = super().validate(attrs) data = super().validate(attrs)
data["name"] = self.user.name data["name"] = self.user.name
data["userId"] = self.user.id data["userId"] = self.user.id
@@ -114,8 +122,8 @@ class LoginSerializer(TokenObtainPairSerializer):
user.login_error_count += 1 user.login_error_count += 1
if user.login_error_count >= 5: if user.login_error_count >= 5:
user.is_active = False user.is_active = False
raise CustomValidationError("账号已被锁定,联系管理员解锁")
user.save() user.save()
raise CustomValidationError("账号已被锁定,联系管理员解锁")
count = 5 - user.login_error_count count = 5 - user.login_error_count
raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~") raise CustomValidationError(f"账号/密码错误;重试{count}次后将被锁定~")

View File

@@ -16,6 +16,8 @@ from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
class MenuButtonSerializer(CustomModelSerializer): class MenuButtonSerializer(CustomModelSerializer):
""" """
菜单按钮-序列化器 菜单按钮-序列化器
@@ -92,16 +94,14 @@ class MenuButtonViewSet(CustomModelViewSet):
""" """
menu_obj = Menu.objects.filter(id=request.data['menu']).first() menu_obj = Menu.objects.filter(id=request.data['menu']).first()
result_list = [ result_list = [
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.web_path}/', {'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.component_name}/', 'method': 1},
'method': 1}, {'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.component_name}/{{id}}/', 'method': 3},
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/', {'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.component_name}/{{id}}/', 'method': 2},
'method': 3}, {'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.component_name}/', 'method': 0},
{'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/', {'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.component_name}/{{id}}/', 'method': 0},
'method': 2}, {'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api{menu_obj.component_name}/', 'method': 1},
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/', {'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api{menu_obj.component_name}/import_data/', 'method': 1},
'method': 0}, {'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},]
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/',
'method': 0}]
serializer = self.get_serializer(data=result_list, many=True) serializer = self.get_serializer(data=result_list, many=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()

View File

@@ -321,86 +321,45 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
is_superuser = request.user.is_superuser is_superuser = request.user.is_superuser
if is_superuser: if is_superuser:
data = [ data = [
{ {"value": 0, "label": '仅本人数据权限'},
"value": 0, {"value": 1, "label": '本部门及以下数据权限'},
"label": '仅本人数据权限' {"value": 2, "label": '本部门数据权限'},
}, {"value": 3, "label": '全部数据权限'},
{ {"value": 4, "label": '自定义数据权限'}
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
] ]
return DetailResponse(data=data) return DetailResponse(data=data)
else: else:
data = [] params = request.query_params
data = [{"value": 0, "label": '仅本人数据权限'}]
role_list = request.user.role.values_list('id', flat=True) role_list = request.user.role.values_list('id', flat=True)
if params := request.query_params: # 权限页面进入初始化获取所有的数据权限范围
role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list
).values_list('data_range', flat=True)
# 通过按钮小齿轮获取指定按钮的权限
if menu_button_id := params.get('menu_button', None): if menu_button_id := params.get('menu_button', None):
role_queryset = RoleMenuButtonPermission.objects.filter( role_queryset = RoleMenuButtonPermission.objects.filter(
role__in=role_list, menu_button__id=menu_button_id role__in=role_list, menu_button__id=menu_button_id
).values_list('data_range', flat=True) ).values_list('data_range', flat=True)
data_range_list = list(set(role_queryset)) data_range_list = list(set(role_queryset))
for item in data_range_list: for item in data_range_list:
if item == 0: if item == 0:
data = [{ data = data
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1: elif item == 1:
data = [{ data.extend([
"value": 0, {"value": 1, "label": '本部门及以下数据权限'},
"label": '仅本人数据权限' {"value": 2, "label": '本部门数据权限'}
}, { ])
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2: elif item == 2:
data = [{ data.extend([{"value": 2, "label": '本部门数据权限'}])
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3: elif item == 3:
data = [{ data.extend([{"value": 3, "label": '全部数据权限'}])
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4: elif item == 4:
data = [{ data.extend([{"value": 4, "label": '自定义数据权限'}])
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else: else:
data = [] data = []
return DetailResponse(data=data) return DetailResponse(data=data)
return ErrorResponse(msg="参数错误")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request): def role_to_dept_all(self, request):
@@ -409,23 +368,23 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
:param request: :param request:
:return: :return:
""" """
params = request.query_params
is_superuser = request.user.is_superuser is_superuser = request.user.is_superuser
if is_superuser: params = request.query_params
queryset = Dept.objects.values('id', 'name', 'parent') role_id = params.get('role')
else: menu_button_id = params.get('menu_button')
if not params: dept_checked = RoleMenuButtonPermission.objects.filter(
return ErrorResponse(msg="参数错误") role_id=role_id, menu_button_id=menu_button_id
menu_button = params.get('menu_button') ).values_list('dept', flat=True)
if menu_button is None: dept_list = Dept.objects.values('id', 'name', 'parent')
return ErrorResponse(msg="参数错误") data = {
role_list = request.user.role.values_list('id', flat=True) 'depts': [],
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_list, menu_button=None).values( 'dept_checked': [i for i in dept_checked if i is not None]
dept_id=F('dept__id'), }
name=F('dept__name'),
parent=F('dept__parent') for dept in dept_list:
) dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked
return DetailResponse(data=queryset) data['depts'].append(dept)
return DetailResponse(data=data)
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def menu_to_button(self, request): def menu_to_button(self, request):

View File

@@ -1,6 +1,6 @@
Django==4.2.7 Django==4.2.7
django-comment-migrate==0.1.7 django-comment-migrate==0.1.7
django-cors-headers==4.3.0 django-cors-headers==4.4.0
django-filter==23.3 django-filter==23.3
django-ranged-response==0.2.0 django-ranged-response==0.2.0
djangorestframework==3.14.0 djangorestframework==3.14.0

View File

@@ -6,4 +6,4 @@ RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" |
RUN sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py RUN sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py
RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py
RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt
CMD ["/backend/docker_start.sh"] CMD ["sh","docker_start.sh"]

View File

@@ -7,6 +7,10 @@ server {
index index.html index.htm; index index.html index.htm;
root /usr/share/nginx/html; root /usr/share/nginx/html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
add_header Cache-Control "no-store";
}
} }
location ~ ^/api/ { location ~ ^/api/ {

View File

@@ -11,6 +11,10 @@ server {
real_ip_header X-Forwarded-For; real_ip_header X-Forwarded-For;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.php index.htm; index index.html index.php index.htm;
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
add_header Cache-Control "no-store";
}
} }
location /api/ { location /api/ {

2
web/.gitignore vendored
View File

@@ -21,3 +21,5 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# 构建版本文件无需上传git
public/version-build

View File

@@ -9,7 +9,7 @@ export default {
two4: '友情链接', two4: '友情链接',
}, },
account: { account: {
accountPlaceholder1: '请输入登录账号', accountPlaceholder1: '请输入登录账号/邮箱/手机号',
accountPlaceholder2: '请输入登录密码', accountPlaceholder2: '请输入登录密码',
accountPlaceholder3: '请输入验证码', accountPlaceholder3: '请输入验证码',
accountBtnText: '登 录', accountBtnText: '登 录',

View File

@@ -13,6 +13,7 @@ import {initBackEndControlRoutes, setRouters} from '/@/router/backEnd';
import {useFrontendMenuStore} from "/@/stores/frontendMenu"; import {useFrontendMenuStore} from "/@/stores/frontendMenu";
import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes"; import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes";
import {toRaw} from "vue"; import {toRaw} from "vue";
import {checkVersion} from "/@/utils/upgrade";
/** /**
* 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。 * 1、前端控制路由时isRequestRoutes 为 false需要写 roles需要走 setFilterRoute 方法。
@@ -95,6 +96,8 @@ export function formatTwoStageRoutes(arr: any) {
// 路由加载前 // 路由加载前
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
await checkVersion()
NProgress.configure({showSpinner: false}); NProgress.configure({showSpinner: false});
if (to.meta.title) NProgress.start(); if (to.meta.title) NProgress.start();
const token = Session.get('token'); const token = Session.get('token');

View File

@@ -1,5 +1,7 @@
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import '/@/theme/loading.scss'; import '/@/theme/loading.scss';
import { showUpgrade } from "/@/utils/upgrade";
/** /**
* 页面全局 Loading * 页面全局 Loading
@@ -9,6 +11,8 @@ import '/@/theme/loading.scss';
export const NextLoading = { export const NextLoading = {
// 创建 loading // 创建 loading
start: () => { start: () => {
// 显示升级提示
showUpgrade()
const bodys: Element = document.body; const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div'); const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next'); div.setAttribute('class', 'loading-next');

55
web/src/utils/upgrade.ts Normal file
View File

@@ -0,0 +1,55 @@
import axios from "axios";
import * as process from "process";
import {Local, Session} from '/@/utils/storage';
import {ElNotification} from "element-plus";
import fs from "fs";
// 是否显示升级提示信息框
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
const VERSION_KEY = 'DVADMIN3_VERSION'
const VERSION_FILE_NAME = 'version-build'
export function showUpgrade () {
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
if (isShowUpgrade) {
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY)
ElNotification({
title: '新版本升级',
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!",
type: 'success',
duration: 5000,
});
}
}
// 生产环境前端版本校验,
export async function checkVersion(){
if (process.env.NODE_ENV === 'development') {
// 开发环境无需校验前端版本
return
}
// 获取线上版本号 t为时间戳防止缓存
await axios.get(`/${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => {
const {status, data} = res || {}
if (status === 200) {
// 获取当前版本号
const localVersion = Local.get(VERSION_KEY)
// 将当前版本号持久缓存至本地
Local.set(VERSION_KEY, data)
// 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作
if (localVersion && localVersion !== data) {
// 本地缓存版本号和线上版本号不一致,弹出升级提示框
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true)
window.location.reload()
}
}
})
}
export function generateVersionFile (){
// 生成版本文件到public目录下version文件中
const version = `${process.env.npm_package_version}.${new Date().getTime()}`;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
}

View File

@@ -80,8 +80,8 @@ export default defineComponent({
const state = reactive({ const state = reactive({
isShowPassword: false, isShowPassword: false,
ruleForm: { ruleForm: {
username: '', username: 'superadmin',
password: '', password: 'admin123456',
captcha: '', captcha: '',
captchaKey: '', captchaKey: '',
captchaImgBase: '', captchaImgBase: '',

View File

@@ -42,10 +42,11 @@ export function getDataPermissionRangeAll() {
method: 'get', method: 'get',
}) })
} }
export function getDataPermissionDept() { export function getDataPermissionDept(query:object) {
return request({ return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/', url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get' method: 'get',
params:query
}) })
} }

View File

@@ -29,7 +29,7 @@
:label="btn.value"> :label="btn.value">
<div class="btn-item"> <div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }} {{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menu, btn.id)"> <span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menu, btn)">
<el-icon> <el-icon>
<Setting /> <Setting />
</el-icon> </el-icon>
@@ -124,6 +124,7 @@ watch(
(val) => { (val) => {
drawerVisible.value = val; drawerVisible.value = val;
getMenuBtnPermission() getMenuBtnPermission()
getDataPermissionRangeLable()
} }
); );
@@ -144,9 +145,10 @@ let menuCurrent = ref<Partial<MenuDataType>>({});
let menuBtnCurrent = ref<number>(-1); let menuBtnCurrent = ref<number>(-1);
let dialogVisible = ref(false); let dialogVisible = ref(false);
let dataPermissionRange = ref<DataPermissionRangeType[]>([]); let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
let dataPermissionRangeLabel = ref<DataPermissionRangeType[]>([]);
const formatDataRange = computed(() => { const formatDataRange = computed(() => {
return function (datarange: number) { return function (datarange: number) {
const findItem = dataPermissionRange.value.find((i) => i.value === datarange); const findItem = dataPermissionRangeLabel.value.find((i) => i.value === datarange);
return findItem?.label || '' return findItem?.label || ''
} }
}) })
@@ -158,8 +160,13 @@ const getMenuBtnPermission = async () => {
const resMenu = await getRolePermission({ role: props.roleId }) const resMenu = await getRolePermission({ role: props.roleId })
menuData.value = resMenu menuData.value = resMenu
} }
// 获取按钮的数据权限下拉选项
const getDataPermissionRangeLable = async () => {
const resRange = await getDataPermissionRange({ role: props.roleId })
dataPermissionRangeLabel.value = resRange.data;
}
const fetchData = async (btnId) => { const fetchData = async (btnId:number) => {
try { try {
const resRange = await getDataPermissionRange({menu_button:btnId}); const resRange = await getDataPermissionRange({menu_button:btnId});
if (resRange?.code === 2000) { if (resRange?.code === 2000) {
@@ -170,20 +177,22 @@ const fetchData = async (btnId) => {
} }
}; };
const handleCollapseChange = (val: number) => { // const handleCollapseChange = (val: number) => {
collapseCurrent.value = [val]; // collapseCurrent.value = [val];
}; // };
/** /**
* 设置按钮数据权限 * 设置按钮数据权限
* @param record 当前菜单 * @param record 当前菜单
* @param btnType 按钮类型 * @param btnType 按钮类型
*/ */
const handleSettingClick = (record: MenusType, btnId: number) => { const handleSettingClick = (record: MenusType, btn: MenusType['btns'][number]) => {
menuCurrent.value = record; menuCurrent.value = record;
menuBtnCurrent.value = btnId; menuBtnCurrent.value = btn.id;
dialogVisible.value = true; dialogVisible.value = true;
fetchData(btnId) dataPermission.value =btn.data_range;
handlePermissionRangeChange(btn.data_range)
fetchData( btn.id)
}; };
const handleColumnChange = (val: boolean, record: MenusType, btnType: string) => { const handleColumnChange = (val: boolean, record: MenusType, btnType: string) => {
@@ -194,9 +203,10 @@ const handleColumnChange = (val: boolean, record: MenusType, btnType: string) =>
const handlePermissionRangeChange = async (val: number) => { const handlePermissionRangeChange = async (val: number) => {
if (val === 4) { if (val === 4) {
const res = await getDataPermissionDept(); const res = await getDataPermissionDept({ role: props.roleId,menu_button:menuBtnCurrent.value });
const data = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false }); const depts = XEUtils.toArrayTree(res.data.depts, { parentKey: 'parent', strict: false });
deptData.value = data; deptData.value = depts;
customDataPermission.value = res.data.dept_checked;
} }
}; };

View File

@@ -3,6 +3,7 @@ import { resolve } from 'path';
import { defineConfig, loadEnv, ConfigEnv } from 'vite'; import { defineConfig, loadEnv, ConfigEnv } from 'vite';
import vueSetupExtend from 'vite-plugin-vue-setup-extend'; import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import { generateVersionFile } from "/@/utils/upgrade";
const pathResolve = (dir: string) => { const pathResolve = (dir: string) => {
return resolve(__dirname, '.', dir); return resolve(__dirname, '.', dir);
@@ -17,6 +18,8 @@ const alias: Record<string, string> = {
const viteConfig = defineConfig((mode: ConfigEnv) => { const viteConfig = defineConfig((mode: ConfigEnv) => {
const env = loadEnv(mode.mode, process.cwd()); const env = loadEnv(mode.mode, process.cwd());
// 当Vite构建时生成版本文件
generateVersionFile()
return { return {
plugins: [vue(), vueJsx(), vueSetupExtend()], plugins: [vue(), vueJsx(), vueSetupExtend()],
root: process.cwd(), root: process.cwd(),