一步一步教你用Django搭建一个后台管理系统
一、团队协作、项目初始化
1、小组负责人在本地新建一个Djang项目
// 新建项目
django-admin startproject inventory_management_django_system
2、在gitee上新建仓库,将本地项目上传到新建的gitee仓库中
- 这部分参考博客:blog.****.net/qq_48278620…
3、团队成员协作开发(前提:团队成员电脑上已安装Git和Gitee账号已用邮箱注册)
- 项目管理员在仓库分支管理中手动添加分支【分支名称】
- 在想要放置项目的文件夹里右键,执行git init命令进行初始化
- 执行git clone 【项目地址】 把项目克隆到本地
- 成员进入项目文件夹,在此目录下依次执行以下几条命令:
git checkout 【分支名称】
git add .
git commit -m "描述内容“
git pull --rebase origin 【分支名称】
git push -u origin 【分支名称】
此时成员的本地项目便与仓库中属于自己的分支相关联了
- 进入PythonCharm,下载Gitee插件,项目更改后可以将项目右键选择提交与推送到自己的分支
- 如果推送时出现下面的错误提示:
而且输入密码后仍然像下面显示的那样,推送失败:
此时尝试执行以下命令,清除本地的gitee用户名和密码:
git config --system --unset credential.helper
如果仍然推送失败,重启PyCharm,重新进行推送。
二、数据库建表工作
1、创建并注册这部分模块的app
- 创建app:
//创建app(app的名字自定义)
python .\manage.py startapp app01
- 注册app(settings.py里的INSTALLED_APPS代码块如图修改):
- 运行这个初始化项目:
//运行Django项目
python .\manage.py runserver
2、创建并连接数据库
- 在pgAdmin 4手动创建数据库:
- 项目设置文件settings.py中更改DATABASE部分代码如下:
3、在创建表之前观察后台管理系统需要完成的功能
- 本系统为一个简单的后台管理系统,可为企业提供后台管理服务,主要包括系统用户的登录注册、库存管理、订单管理、临时订单、用户管理、数据可视化六大板块的功能。
- 其中系统用户分为两类,一类是高权限的超级管理员,另一类是低权限的普通管理员,其中普通管理员不具备用户管理功能。系统用户既是系统的管理员,也是订单的负责人,每个管理员负责自己的订单与临时订单。
- 在注册页面默认注册低权限的普通管理员,超级管理员可在用户管理板块中对系统的用户权限进行修改。
- 每个订单/临时订单会通过userId这个属性与用户绑定,类似于公司中某个订单是谁负责的,增加订单/临时订单时通过验证目前所登录信息来做到绑定。
- 数据可视化模块中,主要对公司近十年具体的销售额数据进行可视化,里面的数据通过sql语句初始化在数据库表里面,前端无需提供,后端也无法更改,相当于一个独立静态的模块,无数据的改变。
登录注册:(涉及系统用户user表)——完成系统的登录、登录之后所登录用户信息的返回、系统用户的注册
库存管理:(涉及物品ware表)——完成对库存物品的增、删、改、查,以及选择物品加入订单或者临时订单
订单管理:(涉及订单order表、系统用户user表、临时订单cart表)——完成对订单的删、查
临时订单:(涉及临时订单cart表、订单order表、系统用户user表)——完成临时订单的删除、编辑,以及选择临时订单加入订单仓库
用户管理:(涉及系统用户user表)——完成对系统用户的增、删、改、查(注意:只有高权限的系统用户才具有这项功能,在登录时便可进行验证所登录用户是否为高权限用户;【增加用户】与【注册用户】本质是同一个功能)
数据可视化:(涉及公司销售额sales表)——完成公司近十年每年具体销售数据的可视化
4、项目中需要创建的表
从上面展示的六大模块功能可以看出,项目一共涉及五个表,分别是系统用户user表、物品ware表、订单order表、临时订单cart表、公司销售额sales表。
其中每个表的属性介绍如下:
- 系统用户user表:【userId,userName,userPassword,userPower,createTime,updateTime
- 物品ware表:【wareId,wareName,warePower,wareCount,createTime,updateTime】
- 订单order表:【orderId,userId,userName,wareId,wareName,wareCount,createTime】
- 临时订单cart表:【orderId,userId,userName,wareId,wareName,wareCount,createTime,updateTime】
- 公司销售额sales表:【salesId,yearName,yearSales,yearEvents,monthSales,wareSales】
注意:订单order表与临时订单cart表本质是同样的表,但它们的区别是,选择物品加入订单时,仓库中对应的物品会减少,而选择物品加入临时订单时,仓库中对应的物品不会减少。
5、进行表的创建
在项目app文件夹里面的models.py文件进行表模型的构造:
(有关这部分的文档可见:docs.djangoproject.com/zh-hans/4.1…)
本项目的models.py如下:
from django.db import models
from django.db.models import Max, Count
# 导入`connection`用于执行原生sql语句
from django.db import connection
# Create your models here.
# 系统用户user表:【userId,userName,userPassword,userPower,createTime,updateTime】
class User(models.Model):
# primary_key=True表示该属性为该表的主键,暗示null=False和unique=True。
id = models.AutoField(primary_key=True)
userId = models.CharField(max_length=20)
userName = models.CharField(unique=True, max_length=15)
userPassword = models.CharField(max_length=20)
userPower = models.DecimalField(max_digits=3, decimal_places=0, default=10)
createTime = models.DateTimeField(auto_now_add=True)
updateTime = models.DateTimeField(auto_now=True)
def save(self, **kwargs):
if not self.id:
idCount = User.objects.aggregate(Count('id')).get("id__count")
cursor = connection.cursor()
if idCount == 0:
# 要想使用sql原生语句,必须用到execute()函数,然后在里面写入sql原生语句
cursor.execute("TRUNCATE app_user RESTART IDENTITY")
maxid = User.objects.aggregate(Max('id')).get("id__max")
# 让主键从什么位置开始排序
if maxid is not None:
cursor.execute("ALTER SEQUENCE app_user_id_seq RESTART WITH %s", [maxid+1])
self.userId = "{}{:06d}".format('user', (maxid+1) if maxid is not None else 1)
super().save(*kwargs)
# 物品ware表:【wareId,wareName,warePower,wareCount,createTime,updateTime】
class Ware(models.Model):
id = models.AutoField(primary_key=True)
wareId = models.CharField(max_length=20)
wareName = models.CharField(unique=True, max_length=15)
warePower = models.DecimalField(max_digits=8, decimal_places=0, default=0)
wareCount = models.DecimalField(max_digits=10, decimal_places=0, default=0)
createTime = models.DateTimeField(auto_now_add=True)
updateTime = models.DateTimeField(auto_now=True)
def save(self, **kwargs):
if not self.id:
idCount = Ware.objects.aggregate(Count('id')).get("id__count")
cursor = connection.cursor()
if idCount == 0:
# 要想使用sql原生语句,必须用到execute()函数,然后在里面写入sql原生语句
cursor.execute("TRUNCATE app_ware RESTART IDENTITY")
maxid = Ware.objects.aggregate(Max('id')).get("id__max")
# 让主键从什么位置开始排序
if maxid is not None:
cursor.execute("ALTER SEQUENCE app_ware_id_seq RESTART WITH %s", [maxid+1])
self.wareId = "{}{:06d}".format('ware', (maxid+1) if maxid is not None else 1)
super().save(*kwargs)
# 订单order表:【orderId,userId,userName,wareId,wareName,wareCount,createTime】
class Order(models.Model):
id = models.AutoField(primary_key=True)
orderId = models.CharField(max_length=20)
userId = models.CharField(max_length=20)
userName = models.CharField(max_length=15)
wareId = models.CharField(max_length=20)
wareName = models.CharField(max_length=15)
wareCount = models.DecimalField(max_digits=10, decimal_places=0, default=0)
createTime = models.DateTimeField(auto_now_add=True)
def save(self, **kwargs):
if not self.id:
idCount = Order.objects.aggregate(Count('id')).get("id__count")
cursor = connection.cursor()
if idCount == 0:
# 要想使用sql原生语句,必须用到execute()函数,然后在里面写入sql原生语句
cursor.execute("TRUNCATE app_order RESTART IDENTITY")
maxid = Order.objects.aggregate(Max('id')).get("id__max")
# 让主键从什么位置开始排序
if maxid is not None:
cursor.execute("ALTER SEQUENCE app_order_id_seq RESTART WITH %s", [maxid+1])
self.orderId = "{}{:06d}".format('order', (maxid+1) if maxid is not None else 1)
super().save(*kwargs)
# 临时订单cart表:【cartId,userId,userName,wareId,wareName,wareCount,createTime,updateTime】
class Cart(models.Model):
id = models.AutoField(primary_key=True)
cartId = models.CharField(max_length=20)
userId = models.CharField(max_length=20)
userName = models.CharField(max_length=15)
wareId = models.CharField(max_length=20)
wareName = models.CharField(max_length=15)
wareCount = models.DecimalField(max_digits=10, decimal_places=0, default=0)
createTime = models.DateTimeField(auto_now_add=True)
updateTime = models.DateTimeField(auto_now=True)
def save(self, **kwargs):
if not self.id:
idCount = Cart.objects.aggregate(Count('id')).get("id__count")
cursor = connection.cursor()
if idCount == 0:
# 要想使用sql原生语句,必须用到execute()函数,然后在里面写入sql原生语句
cursor.execute("TRUNCATE app_cart RESTART IDENTITY")
maxid = Cart.objects.aggregate(Max('id')).get("id__max")
# 让主键从什么位置开始排序
if maxid is not None:
cursor.execute("ALTER SEQUENCE app_cart_id_seq RESTART WITH %s", [maxid+1])
self.cartId = "{}{:06d}".format('cart', (maxid+1) if maxid is not None else 1)
super().save(*kwargs)
# 公司销售额sales表:【salesId,yearName,yearSales,yearEvents,monthSales,wareSales】
class Sales(models.Model):
id = models.AutoField(primary_key=True)
salesId = models.CharField(max_length=20)
yearName = models.CharField(max_length=5)
yearSales = models.DecimalField(max_digits=10, decimal_places=2, default=0)
yearEvents = models.CharField(max_length=30, default="")
monthSales = models.JSONField(null=True)
wareSales = models.JSONField(null=True)
def save(self, **kwargs):
if not self.id:
idCount = Sales.objects.aggregate(Count('id')).get("id__count")
cursor = connection.cursor()
if idCount == 0:
# 要想使用sql原生语句,必须用到execute()函数,然后在里面写入sql原生语句
cursor.execute("TRUNCATE app_sales RESTART IDENTITY")
maxid = Sales.objects.aggregate(Max('id')).get("id__max")
# 让主键从什么位置开始排序
if maxid is not None:
cursor.execute("ALTER SEQUENCE app_sales_id_seq RESTART WITH %s", [maxid+1])
self.salesId = "{}{:06d}".format('sales', (maxid+1) if maxid is not None else 1)
super().save(*kwargs)
//之后依次执行以下两条语句完成表的创建完成
python .\manage.py makemigrations
python .\manage.py migrate
此时在pgAdmin4中可以看到,表的创建完成:
三、登录注册模块的开发
API文档:docs.djangoproject.com/zh-hans/4.1…(包含: 返回新 QuerySet 的方法 , 返回新 QuerySet 的操作符 , 不返回 QuerySet 的方法 , Field 查找 , 聚合函数 , 查询相关工具等各种相关查询工具的API)
1、注册功能
注册功能——前端提交一个User类,类似这种:
实际上后端只需要拿到userId、userName这两项,剩下的属性靠后端处理得到,再整合到一起,将整合后的结果存储到数据库对应的表中。后端需要处理的属性是createTime、updateTime、userId、userPower,实际上到这步已经无需理会这几项,因为在建user表时,就已经做出了相应处理:
- userPower字段中的deafult = 10属性表示注册的用户默认权限为10(没有额外传另外的值时);
- auto_now_add=True表示当一条新数据被创建成功后,将该入参auto_now_add对应的表字段的值设置为创建成功时的系统时间,以后修改这条新数据时,该表字段的值不会再更新;
- auto_now=True当一条新数据被修改成功后,将该入参auto_now对应的表字段的值设置为修改成功时的系统时间;
- 重写的save函数表示新建一条数据时,自动生成并保存诸如:“userXXXXXX”这种唯一确定的userId。
下面在app文件夹里面的view.py写注册功能函数:
# 导入所需模块
import json
from app01.models import *
# 处理跨域
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from django.shortcuts import render
# Create your views here.
# 注册功能
@csrf_exempt #处理跨域
def register(request):
# 获取前端传过来的值--request.body表示前端传过来的值,.decode()表示使中文不乱码,用json.loads转换为json格式
reqBody = json.loads(request.body.decode())
# print(reqBody)
username = reqBody['userName']
password = reqBody['userPassword']
if username and password:
# 判断字符串长度
if len(username) > 15 or len(password) > 20:
return JsonResponse({'code': -1, 'msg': '用户名或密码过长'})
# 查找数据库中是否已经存在相同的userName
user_name = User.objects.filter(userName=username)
if user_name.exists():
return JsonResponse({'code': -1, 'msg': '用户已存在'})
# 如果不重复,在保存有关信息
uesr_save = User(userName=username, userPassword=password)
uesr_save.save()
# 返回注册成功信息给前端
return JsonResponse({'code': 0, 'msg': 'success'})
else:
return JsonResponse({'code': -1, "msg": "用户名或密码不能为空"})
然后在urls.py注册路由:
之后运行目前的Django项目:
python .\manage.py runserver
之后我们去测试写的注册功能接口,用【postman】这个软件来测试:
发现报错,因为涉及到跨域问题,解决办法:
在视图views.py里面加上下面两行:
此时,重新在postman上发起请求,发现请求成功:
再去数据库查看数据,发现数据已经进入数据库:
测试注册一个已存在的用户,也成功:
至此,注册功能已完成。
2、登录功能
注册功能——前端提交一个User类,类似这种:
实现登录功能与其它功能操作上相差无几,就是多一个redis缓存的使用,方便获取目前所登录用户的信息,后端返回一个cookie。
步骤如下:
- 安装Redis
- 在settings.py里面配置redis:
# 配置Django缓存存储
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
# 让Django默认使用缓存作为存储session储存后端,这样也不需要安装任何额外的 backend
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
- 在views.py里面编写登录函数
# 导入所需模块
from app01.models import *
# 处理跨域
from django.views.decorators.csrf import csrf_exempt
# JSON对象序列化
import json
from django.core import serializers
from django.http import JsonResponse
# 防止重复登录
from django.contrib.sessions.models import Session
from django.utils import timezone
# 登录功能
@csrf_exempt # 处理跨域
def userLogin(request):
# 获取前端传过来的值--request.body表示前端传过来的值,.decode()表示使中文不乱码,用json.loads转换为json格式
reqBody = json.loads(request.body.decode())
# print(reqBody)
username = reqBody['userName']
password = reqBody['userPassword']
if username and password:
# 查找符合条件的用户信息
user = User.objects.filter(userName=username)
if user.exists():
if user.first().userPassword != password:
return JsonResponse({'code': 0, 'msg': '密码输入错误'})
else:
userInfo = (json.loads(serializers.serialize("json", user)))[0]
# 当前的所有session--防止重复登录
valid_session_obj_list = Session.objects.filter(expire_date__gt=timezone.now())
flag = 0
for session_obj in valid_session_obj_list:
print(session_obj.get_decoded().get("userInfo"))
if session_obj.get_decoded().get("userInfo").pk == userInfo.pk:
flag = 1
break
if flag == 0:
# 将所登录的用户信息存储在session中(已经改session的默认存储位置为redis中),返回给前端的cookie是sessionid
# 只要前端每次请求都带上cookie,后端便可根据每个用户的sessionid查找或者操作对应的登录状态和登录信息
request.session['userInfo'] = userInfo
# 这句话完成了下面几步:
# 1.生成随机的sessionid字符串
# 2.将sessionid和用户的信息在数据库中保存为一个键值对
# 3.通过cookie将sessionid保存在客户端上
# 这时候通过用户再次向服务器发送请求时服务器就可以通过请求中的sessionid判断用户的信息了,从而达到保存登录状态的要求。
# 返回登录成功信息给前端
return JsonResponse({'code': 0, 'msg': 'success', 'userInfo': userInfo})
else:
return JsonResponse({'code': -1, 'msg': '该用户已登录', 'userInfo': userInfo})
else:
return JsonResponse({'code': -1, "msg": "该用户不存在"})
else:
return JsonResponse({'code': -1, "msg": "用户名或密码不能为空"})
- 在urls.py里面注册路由
path("miserauth/login", views.userLogin),
- 运行项目并在postman里测试接口
可以看出,登录成功,并成功设置cookie。
get知识点1:将XX.objects.filter查找到的数据转换为JSON格式的方法
# 1--导入json和serializers模块
import json
from django.core import serializers
# 2--获取数据(此时获取到的是符合要求的对象集合),user.first()表示该集合的第一个对象
user = User.objects.filter(userName=username)
# 3--将对象集合转为JSON格式
# (serializers.serialize表示将对象集合转化为JSON字符串,json.loads表示将该字符串转为真正的JSON格式,[0]表示取得到的数组中的第0个)
return JsonResponse({'code': 0, 'msg': 'success', 'userInfo': (json.loads(serializers.serialize("json", user)))[0]})
# 4--输出如下:
{
"code": 0,
"msg": "success",
"userInfo": {
"model": "app01.user",
"pk": 1,
"fields": {
"userId": "user000001",
"userName": "scucsyyds",
"userPassword": "123456",
"userPower": "10",
"createTime": "2022-11-23T04:45:02.747Z",
"updateTime": "2022-11-23T04:45:02.747Z"
}
}
}
get知识点2:Redis下载和安装(Windows系统)
参考链接:c.biancheng.net/redis/windo…
(注意:在执行redis-server.exe --service-start前要把客户端启动关掉,否则端口会被占用)
get知识点3:windows免费安装redis desktop manager
上github找到免费版最新的是2018年的,可以直接下载exe执行程序:github.com/uglide/Redi…
3、获取当前登录信息
获取获取当前登录信息——前端只发起get请求,无需提交任何东西;后端从redis中返回登录用户的信息。
获得当前登录信息,需要用到的地方:
1、登录时验证是否为超级管理员,是的话开放用户管理功能,同时临时订单展示所有;不是则关闭用户管理功能,同时临时订单展示该管理员对应的临时订单。
2、在物品管理页面加入订单或者临时订单时,绑定当前所登录用户信息
主要思想就是根据前端请求携带的cookie,通过cookie中的sessionid,在redis中找到对应的登录信息。
# 获取当前登录信息
@csrf_exempt # 处理跨域
def getLoginUser(request):
try:
userInfo = request.session.get('userInfo', None)
if userInfo:
# 返回登录成功信息给前端
return JsonResponse({'code': 0, 'msg': 'success', 'userInfo': userInfo})
else:
return JsonResponse({'code': -1, "msg": "未查找到登录信息"})
# 防止异常处理太过宽泛
except AttributeError:
return JsonResponse({'code': -2, 'msg': '发生异常'})
登录之后用postman测试该功能:
可以看出,获取登录用户信息,该接口是成功的。
4、退出登录功能
主要思想将后端redis缓存的登录状态cookie清除掉,返回退出成功信息给前端。
# 退出登录
@csrf_exempt # 处理跨域
def loginOut(request):
try:
# 退出登录的前提是已经登录
userInfo = request.session.get('userInfo', None)
if userInfo:
# 清除该用户在服务器中的session,删除客户端的sessionid在服务器中保存的状态
del request.session['userInfo']
# 返回退出成功信息给前端
return JsonResponse({'code': 0, 'msg': 'success'})
else:
return JsonResponse({'code': -1, 'msg': '当前用户未登录'})
# 防止异常处理太过宽泛
except AttributeError:
return JsonResponse({'code': -2, 'msg': '退出失败'})
此时用postman测试该功能:
然后再去获取登录用户信息:
可以看出,目前没有登录信息,接口是成功的。
四、数据可视化模块的开发
1、将模拟数据存入数据库
首先定义项目中静态文件的位置:
然后在setting.py里面注册静态文件的路径:
STATIC_URL = "static/"
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
接着写好模拟数据的JSON,如下所示:
在views.py写好存入数据库的函数:
# 将销售数据传入数据库
@csrf_exempt # 处理跨域
def saveSalesData(request):
# 获取前端传过来的值--request.body表示前端传过来的值,.decode()表示使中文不乱码,用json.loads转换为json格式
reqBody = json.loads(request.body.decode())
# print(reqBody)
try:
# 查找数据库中是否已经存在相同的yearName
year_name = Sales.objects.filter(yearName=reqBody['yearName'])
if year_name.exists():
return JsonResponse({'code': -1, 'msg': '该年数据已存在'})
# 如果不重复,在保存有关信息
sales_save = Sales(yearName=reqBody['yearName'],
yearSales=reqBody['yearSales'],
yearEvents=reqBody['yearEvents'],
monthSales=reqBody['monthSales'],
wareSales=reqBody['wareSales']
)
sales_save.save()
return JsonResponse({'code': 0, 'msg': 'success'})
# 防止异常处理太过宽泛
except AttributeError:
return JsonResponse({'code': -1, 'msg': '保存失败'})
在urls.py注册路由:
path("saveSalesData", views.saveSalesData),
用postman发起请求来存入数据:
去数据库中查看数据:
可以看出,该接口成功。
2、获取数据库中的数据
一共三个不同的可视化图表,但切换时,前端只发起同一个请求,后端返回sales表中的全部数据供前端处理(反正数据也不多,这个表本就是相当于独立静态的)
在views.py写好获取销售数据函数:
# 获取数据库中的销售数据
@csrf_exempt # 处理跨域
def getSalesData(request):
try:
salesData = (json.loads(serializers.serialize("json", Sales.objects.all())))
return JsonResponse({'code': 0, 'msg': 'success', 'salesData': salesData})
# 防止异常处理太过宽泛
except AttributeError:
return JsonResponse({'code': -1, 'msg': '获取失败'})
在urls.py注册路由:
path("getSalesData", views.getSalesData)
运行项目,并在postman里测试接口:
可以看到,数据获取成功,该接口有效。
五、库存管理模块的开发
获取仓库中的全部物品信息——前端只发起get请求,无需提交任何东西
获取仓库中某个物品信息——前端只需提交一个wareId来发起get请求,类似这种:
增加物品——前端提交一个Ware类,类似这种:(后端只需拿到wareName、wareCount、warePower,剩下的属性在models里面已经处理好了)
删除一个或多个物品——前端提交wareId的数组,类似这种:
编辑某个物品信息,前端提交一个Ware类,类似这种:(后端只需拿到更新后的wareName、wareCount、warePower,剩下的属性在models里面已经处理好了)
六、用户管理模块的开发
注意:该模块只对超级管理员开放,前端来做这个判断处理即可
获取系统中的全部用户信息——前端只发起get请求,无需提交任何东西
获取系统中某个用户信息——前端只需提交一个userId来发起get请求,类似这种:
增加用户——前端提交一个User类,类似这种:(后端只需拿到userName、userPassword、userPower,剩下的属性在models里面已经处理好了)
删除一个或多个物品——前端提交userId的数组,类似这种:
编辑某个用户信息,前端提交一个User类,类似这种:(后端只需拿到更新后的userName、userPassword、userPower,剩下的属性在models里面已经处理好了)
七、订单管理模块的开发
获取系统中的全部订单信息——前端只发起get请求,无需提交任何东西
获取系统中某个订单信息——前端只需提交一个orderId来发起get请求,类似这种:
增加订单——前端提交一个Order类,类似这种:(后端只需拿到userId、userName、wareCount、wareId、wareName,剩下的属性在models里面已经处理好了,注意此时仓库中对应wareId物品的数量会随之减少)
删除一个或多个订单——前端提交orderId的数组,类似这种:
八、临时订单模块的开发
获取系统中的临时订单信息——前端只发起get请求,无需提交任何东西(这个功能需要需要注意的点是,对于超级管理员返回全部临时订单信息,对于普通管理员返回该管理员对应的临时订单信息——后端通过匹配目前的登录信息userId做到)
增加临时订单——前端提交一个Cart类,类似这种:(后端只需拿到userId、userName、wareCount、wareId、wareName,剩下的属性在models里面已经处理好了)
删除一个或多个临时订单——前端提交cartId的数组,类似这种:
编辑某个临时订单信息,前端提交一个Cart类,类似这种:(后端只需拿到更新后的wareCount,剩下的属性不变)
将临时订单加入订单,前端提交一个Cart类数组,类似这种:(后端只需拿到该数组中每个项中的userId、userName、wareCount、war
推荐阅读
-
一步一步教你用Django搭建一个后台管理系统
-
一步步完成搭建图纸管理系统(Django+Vue3)--2、前端页面初建、vue初探
-
openEuler郑州用户组成立!openEuler与hyperfusion携手共建河南地区用户生态 - 开幕致辞 超融合操作系统业务总经理、openEuler委员会成员蒋振华先生为本次活动致辞。 在本次活动的致辞中,他提到,作为openEuler社区早期的成员,超融合见证了openEuler从成立到在各行业商业落地,再到跨越生态拐点的过程,感谢openEuler提供了一个全产业链共同创新的平台,共同推动创新技术的商业落地。 同时,本次活动得到了郑州市郑东新区大数据管理局、郑州中原科技城投资服务局的大力支持。 郑东新区大数据管理局曹光远 在活动致辞中表示,openEuler的应用和*应用设施的深度优化,为郑东新区数字化转型提供了安全、可靠、高性能的技术基础;郑州中原科技城招商服务局王林表示,郑东新区欢迎所有openEuler生态相关企业扎根当地,围绕openEuler社区共同发展,形成合力。 openEuler社区及运维功能介绍 openEuler技术委员会委员胡峰 openEuler技术委员会委员胡峰先生在本次活动中介绍了openEuler社区目前发展的整体情况,并重点从技术层面介绍了openEuler的运维功能。 openEuler 晚会 胡峰先生介绍智能运维工具 A-Ops 和 openEuler gala、 阿波罗 Apollo、智能漏洞管理解决方案等新功能,以及涵盖各种运维场景的精品运维组件。在*交流环节,许多用户就目前使用的 openEuler 在*交流环节,许多用户就自己在使用openEuler过程中遇到的一些问题与胡峰先生进行了进一步的交流。 软硬结合,构建多样化算力操作系统 Hyperfusion 基于 openEuler 的基础上,结合自身软硬件技术积累,推出了富讯服务器操作系统 FusionOS FusionOS. FusionOS 首席架构师张海亮 分享了 FusionOS FusionOS首席架构师张海亮分享了FusionOS的软硬件协同优势、卓越的性能和可靠性,以及FusionOS在金融、运营商、*、互联网等行业的实践案例,引起了众多用户的兴趣,分享结束后,不少参会者就FusionOS的特点向讲师提问并进行了交流。
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
从零开始学Django:一步步掌握模型、API与后台管理系统的实战指南 - 完整教程详解
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。