feat: update

This commit is contained in:
Czw 2023-10-08 18:02:28 +08:00
parent 092e1895a0
commit 56a472adc5
15 changed files with 391 additions and 112 deletions

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
django = "*"
django = "==3.2.16"
djangorestframework = "*"
djangorestframework-simplejwt = "*"
drf-spectacular = "*"
@ -17,6 +17,7 @@ number-precision = "*"
openpyxl = "*"
uvicorn = "*"
gunicorn = "*"
tencentcloud-sdk-python = "*"
[dev-packages]
autopep8 = "*"

View file

@ -11,6 +11,9 @@ class Team(Model):
enable_auto_stock_in = BooleanField(default=False, verbose_name='启用自动入库')
enable_auto_stock_out = BooleanField(default=False, verbose_name='启用自动出库')
register_phone = CharField(max_length=32, blank=True, null=True, verbose_name='注册手机号')
register_city = CharField(max_length=32, blank=True, null=True, verbose_name='所在城市')
class PermissionGroup(Model):
"""权限分组"""
@ -64,6 +67,14 @@ class User(Model):
unique_together = [('username', 'team'), ('name', 'team')]
class VerificationCode(Model):
"""验证码"""
phone = CharField(max_length=32, verbose_name='手机号')
code = CharField(max_length=6, verbose_name='验证码')
create_time = DateTimeField(auto_now_add=True, verbose_name='创建时间')
__all__ = [
'Team', 'PermissionGroup', 'Permission', 'Role', 'User',
'Team', 'PermissionGroup', 'Permission', 'Role', 'User', 'VerificationCode',
]

View file

@ -33,8 +33,22 @@ class SetPasswordRequest(Serializer):
new_password = CharField(label='新密码')
class MakeCodeRequest(Serializer):
phone = CharField(label='手机号')
class RegisterRequest(Serializer):
register_city = CharField(label='所在城市')
phone = CharField(label='手机号')
code = CharField(label='验证码')
number = CharField(label='公司编号')
username = CharField(label='用户名')
password = CharField(label='密码')
__all__ = [
'GetTokenRequest', 'GetTokenResponse',
'RefreshTokenRequest', 'RefreshTokenResponse',
'UserInfoResponse', 'SetPasswordRequest',
'MakeCodeRequest', 'RegisterRequest',
]

View file

@ -11,6 +11,11 @@ from extensions.viewsets import *
from apps.system.serializers import *
from apps.system.schemas import *
from apps.system.models import *
from django.utils import timezone
from datetime import timedelta
import random
from scripts.create_user import create_user
from scripts.send_phone_code import send_phone_code
class PermissionGroupViewSet(BaseViewSet, ListModelMixin):
@ -187,6 +192,42 @@ class UserActionViewSet(FunctionViewSet):
return Response(status=status.HTTP_200_OK)
@extend_schema(request=MakeCodeRequest, responses={204: None})
@action(detail=False, methods=['post'])
def make_code(self, request, *args, **kwargs):
"""生产验证码"""
serializer = MakeCodeRequest(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
code = str(random.randint(100000, 999999))
VerificationCode.objects.create(phone=validated_data['phone'], code=code)
send_phone_code(validated_data['phone'], code)
return Response(status=status.HTTP_204_NO_CONTENT)
@extend_schema(request=RegisterRequest, responses={204: None})
@action(detail=False, methods=['post'])
def register(self, request, *args, **kwargs):
"""注册"""
serializer = RegisterRequest(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
if Team.objects.filter(number=validated_data['number']).exists():
raise ValidationError('公司编号已存在')
start_time = timezone.localtime() - timedelta(minutes=10)
if not VerificationCode.objects.filter(
phone=validated_data['phone'], code=validated_data['code'], create_time__gte=start_time).exists():
raise ValidationError('验证码错误或超时')
expiry_time = timezone.localtime() + timedelta(days=3)
create_user(validated_data['number'], validated_data['phone'], validated_data['register_city'],
expiry_time, validated_data['username'], validated_data['password'])
return Response(status=status.HTTP_204_NO_CONTENT)
__all__ = [
'PermissionGroupViewSet',

View file

@ -16,3 +16,12 @@ DATABASES = {
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Tencent 短信接口
SECRET_ID = ''
SECRET_KEY = ''
SMS_SDK_APP_ID = ''
TEMPLATE_ID = ''
SIGN_NAME = ''
REGION = ''

View file

@ -24,6 +24,7 @@ python manage.py createsuperuser
python manage.py runscript init_permission
python manage.py runscript create_user
python manage.py runscript create_test_data
python manage.py runscript send_phone_code
python manage.py runserver
gunicorn project.asgi:application -c configs/gunicorn.py -k uvicorn.workers.UvicornWorker
ps -aux | grep gunicorn | awk '{print $2}'| xargs kill -9

View file

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
@ -12,6 +12,7 @@
"ant-design-vue": "^1.6.5",
"axios": "^0.20.0",
"core-js": "^3.6.5",
"element-china-area-data": "^6.1.0",
"html2canvas": "^1.3.3",
"js-cookie": "^2.2.1",
"jsbarcode": "^3.11.5",

View file

@ -1,32 +1,41 @@
import request from '@/utils/request';
import request from "@/utils/request";
// GetToken
export function getToken(data) {
return request({ url: `/user/get_token/`, method: 'post', data }, false)
return request({ url: `/user/get_token/`, method: "post", data }, false);
}
// RefreshToken
export function refreshToken(data) {
return request({ url: `/user/refresh_token/`, method: 'post', data })
return request({ url: `/user/refresh_token/`, method: "post", data });
}
// GetInfo
export function getInfo(params) {
return request({ url: `/user/info/`, method: 'get', params })
return request({ url: `/user/info/`, method: "get", params });
}
// SetPassword
export function setPassword(data) {
return request({ url: `/user/set_password/`, method: 'post', data })
return request({ url: `/user/set_password/`, method: "post", data });
}
// 常用功能
export function getCommonFunctions(params) {
return request({ url: `/user/common_functions/`, method: 'get', params })
return request({ url: `/user/common_functions/`, method: "get", params });
}
// 设置常用功能
export function setCommonFunctions(data) {
return request({ url: `/user/set_common_functions/`, method: 'post', data })
}
return request({ url: `/user/set_common_functions/`, method: "post", data });
}
// MakeCode
export function makeCode(data) {
return request({ url: `/user/make_code/`, method: "post", data });
}
// Register
export function registerAccount(data) {
return request({ url: `/user/register/`, method: "post", data });
}

View file

@ -1,19 +1,25 @@
export default {
path: '/user',
name: 'user',
component: () => import('@/layouts/UserLayout'),
path: "/user",
name: "user",
component: () => import("@/layouts/UserLayout"),
children: [
{
path: 'login',
name: 'login',
meta: { title: '登录' },
component: () => import('@/views/login/Login'),
path: "login",
name: "login",
meta: { title: "登录" },
component: () => import("@/views/login/Login"),
},
{
path: 'set_password',
name: 'setPassword',
meta: { title: '设置密码' },
component: () => import('@/views/setPassword/SetPassword'),
path: "register",
name: "register",
meta: { title: "注册" },
component: () => import("@/views/register/index"),
},
{
path: "set_password",
name: "setPassword",
meta: { title: "设置密码" },
component: () => import("@/views/setPassword/SetPassword"),
},
],
}
};

View file

@ -14,79 +14,75 @@
</a-form-model>
</div>
<a-row>
<a-row :gutter="[4, 4]">
<a-col :span="14" offset="5">
<a-button type="primary" size="large" :loading="isLoading" style="width: 100%;" @click="login">登录</a-button>
<a-button type="primary" size="large" :loading="isLoading" style="width: 100%" @click="login">登录</a-button>
</a-col>
<a-col :span="14" offset="5" style="text-align: right">
<a @click="$router.push('/user/register')">注册账号</a>
</a-col>
</a-row>
<div style="text-align: center; width: 100%; margin-top: 24px;">
<div style="text-align: center; width: 100%; margin-top: 24px">
<div>试用购买或问题咨询请扫描下方客户经理二维码</div>
<div>
试用购买或问题咨询请扫描下方客户经理二维码
</div>
<div>
<img :src="wechatCustomerService" width="100" style="margin-top: 8px;" />
<img :src="wechatCustomerService" width="100" style="margin-top: 8px" />
</div>
</div>
</div>
</template>
<script>
import { getToken } from '@/api/user';
import Cookies from 'js-cookie';
import { getToken } from "@/api/user";
import Cookies from "js-cookie";
export default {
name: 'Login',
data() {
return {
wechatCustomerService: require('@/assets/wechat_customer_service.png'),
isLoading: false,
form: {
number: '',
username: '',
password: '',
},
rules: {
number: [
{ required: true, message: '请输入公司编号', trigger: 'change' },
],
username: [
{ required: true, message: '请输入用户名', trigger: 'change' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'change' },
],
},
export default {
name: "Login",
data() {
return {
wechatCustomerService: require("@/assets/wechat_customer_service.png"),
isLoading: false,
form: {
number: "",
username: "",
password: "",
},
rules: {
number: [{ required: true, message: "请输入公司编号", trigger: "change" }],
username: [{ required: true, message: "请输入用户名", trigger: "change" }],
password: [{ required: true, message: "请输入密码", trigger: "change" }],
},
};
},
methods: {
initialize() {
document.onkeypress = (e) => {
let code = document.all ? event.keyCode : e.which;
if (code == 13) {
this.login();
return false;
}
};
},
methods: {
initialize() {
document.onkeypress = (e) => {
let code = document.all ? event.keyCode : e.which;
if (code == 13) {
this.login();
return false;
}
}
},
login() {
this.$refs.form.validate(valid => {
if (valid) {
this.isLoading = true;
getToken(this.form).then(data => {
Cookies.set('access', data.access);
Cookies.set('refresh', data.refresh);
this.$router.push('/home');
}).finally(() => {
login() {
this.$refs.form.validate((valid) => {
if (valid) {
this.isLoading = true;
getToken(this.form)
.then((data) => {
Cookies.set("access", data.access);
Cookies.set("refresh", data.refresh);
this.$router.push("/home");
})
.finally(() => {
this.isLoading = false;
});
}
});
},
}
});
},
created() {
this.initialize();
},
}
</script>
},
created() {
this.initialize();
},
};
</script>

View file

@ -0,0 +1,114 @@
<template>
<div>
<div>
<a-form-model ref="form" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 14 }">
<a-form-model-item prop="phone" label="手机号">
<a-input size="large" v-model="form.phone" />
</a-form-model-item>
<a-form-model-item prop="code" label="验证码">
<a-space>
<a-input size="large" v-model="form.code" />
<a-button type="primary" size="large" @click="sendCode">{{ countDown > 0 ? `${countDown} s` : "发送" }}</a-button>
</a-space>
</a-form-model-item>
<a-form-model-item prop="register_city" label="所在城市">
<a-cascader size="large" v-model="form.cityCode" placeholder="" :options="provinceAndCityData" @change="changeCity" />
</a-form-model-item>
<a-form-model-item prop="number" label="公司编号">
<a-input size="large" v-model="form.number" placeholder="公司英文名或拼音缩写" @pressEnter="register" />
</a-form-model-item>
<a-form-model-item prop="username" label="用户名">
<a-input size="large" v-model="form.username" @pressEnter="register" />
</a-form-model-item>
<a-form-model-item prop="password" label="密码">
<a-input-password size="large" v-model="form.password" @pressEnter="register" />
</a-form-model-item>
</a-form-model>
</div>
<a-row :gutter="[4, 4]">
<a-col :span="14" offset="5">
<a-button type="primary" size="large" :loading="isLoading" style="width: 100%" @click="register"> 注册 </a-button>
</a-col>
<a-col :span="14" offset="5" style="text-align: right">
<a @click="$router.push('/user/login')">返回登录</a>
</a-col>
</a-row>
</div>
</template>
<script>
import { makeCode, registerAccount } from "@/api/user";
import { provinceAndCityData } from "element-china-area-data";
export default {
data() {
return {
isLoading: false,
form: {
cityCode: undefined,
register_city: "",
phone: "",
code: "",
number: "",
username: "",
password: "",
},
rules: {
register_city: [{ required: true, message: "请选择城市", trigger: "change" }],
phone: [{ required: true, message: "请输入手机号", trigger: "change" }],
code: [{ required: true, message: "请输入验证码", trigger: "change" }],
number: [{ required: true, message: "请输入公司", trigger: "change" }],
username: [{ required: true, message: "请输入用户名", trigger: "change" }],
password: [{ required: true, message: "请输入密码", trigger: "change" }],
},
countDown: -1,
provinceAndCityData,
};
},
methods: {
changeCity(_, selectedOptions) {
this.form.register_city = selectedOptions.map((item) => item.label).join(" ");
},
register() {
this.$refs.form.validate((valid) => {
if (valid) {
this.isLoading = true;
registerAccount(this.form)
.then((data) => {
this.$message.success("注册成功");
this.$router.push("/user/login");
})
.finally(() => {
this.isLoading = false;
});
}
});
},
sendCode() {
if (/^1[3-9]\d{9}$/.test(this.form.phone)) {
makeCode({ phone: this.form.phone }).then(() => {
this.$message.success("验证码发送成功");
this.startCountDown();
});
} else {
this.$message.warning("手机号错误");
}
},
startCountDown() {
if (this.countDown < 0) {
this.countDown = 60;
} else if (this.countDown == 0) {
this.countDown = -1;
return;
} else {
this.countDown--;
}
setTimeout(() => {
this.startCountDown();
}, 1000);
},
},
};
</script>

View file

@ -1,32 +1,42 @@
asgiref==3.4.1
attrs==21.2.0
autopep8==1.6.0
click==8.0.3
Django==3.2
django-debug-toolbar==3.2.2
django-extensions==3.1.5
django-filter==21.1
djangorestframework==3.12.4
djangorestframework-simplejwt==5.0.0
drf-spectacular==0.21.0
asgiref==3.7.2
attrs==23.1.0
autopep8==2.0.4
certifi==2023.7.22
charset-normalizer==3.3.0
click==8.1.7
Django==3.2.16
django-debug-toolbar==4.2.0
django-extensions==3.2.3
django-filter==23.3
djangorestframework==3.14.0
djangorestframework-simplejwt==5.3.0
drf-spectacular==0.26.5
et-xmlfile==1.1.0
gunicorn==20.1.0
h11==0.12.0
gunicorn==21.2.0
h11==0.14.0
idna==3.4
inflection==0.5.1
jsonschema==4.2.1
jsonschema==4.19.1
jsonschema-specifications==2023.7.1
number-precision==2021.1.30
openpyxl==3.0.9
openpyxl==3.1.2
packaging==23.2
pendulum==2.1.2
Pillow==8.4.0
pycodestyle==2.8.0
PyJWT==2.3.0
pyrsistent==0.18.0
Pillow==10.0.1
pycodestyle==2.11.0
PyJWT==2.8.0
python-dateutil==2.8.2
pytz==2021.3
pytz==2023.3.post1
pytzdata==2020.1
PyYAML==6.0
PyYAML==6.0.1
referencing==0.30.2
requests==2.31.0
rpds-py==0.10.4
six==1.16.0
sqlparse==0.4.2
toml==0.10.2
sqlparse==0.4.4
tencentcloud-sdk-python==3.0.992
tomli==2.0.1
typing_extensions==4.8.0
uritemplate==4.1.1
uvicorn==0.16.0
urllib3==2.0.6
uvicorn==0.23.2

View file

@ -4,15 +4,22 @@ from apps.system.models import *
import pendulum
@transaction.atomic
def create_user(team_number, register_phone, register_city, expiry_time, username, password):
team = Team.objects.create(
number=team_number,
expiry_time=expiry_time,
register_phone=register_phone,
register_city=register_city)
User.objects.create(team=team, username=username, password=make_password(password), name=username, is_manager=True)
def run(*args):
number = input('编号: ')
username = input('用户名: ')
password = input('密码: ')
name = input('名称: ')
activation_days = input('激活天数: ')
expiry_time = pendulum.now().add(days=float(activation_days))
with transaction.atomic():
team = Team.objects.create(number=number, expiry_time=expiry_time)
User.objects.create(team=team, username=username, password=make_password(password),
name=name, is_manager=True)
create_user(number, None, None, expiry_time, username, password)

View file

@ -0,0 +1,49 @@
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.sms.v20210111 import sms_client, models
from extensions.exceptions import ServerError
from tencentcloud.common import credential
import json
from configs.django import SECRET_ID, SECRET_KEY, SMS_SDK_APP_ID, TEMPLATE_ID, SIGN_NAME, REGION
def send_phone_code(phone, code):
"""发送验证码"""
try:
# 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey此处还需注意密钥对的保密
# 代码泄露可能会导致 SecretId 和 SecretKey 泄露并威胁账号下所有资源的安全性。以下代码示例仅供参考建议采用更安全的方式来使用密钥请参见https://cloud.tencent.com/document/product/1278/85305
# 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
cred = credential.Credential(SECRET_ID, SECRET_KEY)
# 实例化一个http选项可选的没有特殊需求可以跳过
httpProfile = HttpProfile()
httpProfile.endpoint = 'sms.tencentcloudapi.com'
# 实例化一个client选项可选的没有特殊需求可以跳过
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
# 实例化要请求产品的client对象,clientProfile是可选的
client = sms_client.SmsClient(cred, REGION, clientProfile)
# 实例化一个请求对象,每个接口都会对应一个request对象
req = models.SendSmsRequest()
params = {
'PhoneNumberSet': [f'+86{phone}'],
'SmsSdkAppId': SMS_SDK_APP_ID,
'SignName': SIGN_NAME,
'TemplateId': TEMPLATE_ID,
'TemplateParamSet': [code],
}
req.from_json_string(json.dumps(params))
# 返回的resp是一个SendSmsResponse的实例与请求对象对应
client.SendSms(req)
except TencentCloudSDKException:
raise ServerError('验证码发送错误')
def run(*args):
phone = input('手机号: ')
code = input('验证码: ')
send_phone_code(phone, code)

View file

@ -103,6 +103,16 @@ DATABASES = {{
'NAME': BASE_DIR / 'db.sqlite3',
}}
}}
"""
file_content += f"""
# Tencent 短信参数
SECRET_ID = ''
SECRET_KEY = ''
SMS_SDK_APP_ID = ''
TEMPLATE_ID = ''
SIGN_NAME = ''
REGION = ''
"""
with open(BASE_DIR / 'configs/django.py', 'w') as file:
file.write(file_content)