如何扩展 Django 默认用户模型

原生的Django用户模型,仅有简单的几个字段,往往难以满足实际应用的需求。比如应用中需要头像、昵称、手机号或微信UnionID等等,就需要开发者自定义扩展。

但是,每个实际应用中的用户模型,又都是最核心模块之一,往往会与其他领域模型发生各种关联关系。比如登录、验证、授权等等。所以,在系统设计之初,就应该考虑清楚。特别是在Django项目中,扩展的原生用户模型,需要是项目的第一个应用。

如何做呢?下面介绍基于Django用户模型中的AbstractUser类,自定义扩展以满足实际工程项目的需求。

用户模型

在创建Django项目后,接着创建第一个应用accounts

1
2
3
4
# 建项目
python manage.py startproject custom_user_model
# 建应用
python manage.py startapp accounts

将我们的自定义用户模型 AccountModel 作为 AbstractUser 的子类。另一个选择是扩展 AbstractBaseUser 类,二者差别在于所包含的字段不同,具体可阅读源码进一步了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class AccountModel(AbstractUser):
nickname = models.CharField('昵称')
mobile = models.CharField('手机号')
# ... 其他字段根据需要扩展

class Meta:
db_table = 'user_accounts'
managed = True'

有了自定义的用户模型,如何关联到 Django 应用体系中呢?答案是 AUTH_USER_MODEL。我们需要在项目的 settings.py 文件中,将 AccountModel 赋给这个变量。赋值方式要求按 应用名.自定义用户模型 格式。类似这样:

1
2
# custom_user_model/settings.py
AUTH_USER_MODEL = 'accounts.AccountModel'

模型管理

自定义的用户模型中,增加了额外的字段,所以我们期望在新增/修改用户的界面中,能够展示出这些额外字段。办法是,自定义新增/修改用户的表单类,并将其关联到用户模型管理界面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from accounts.models import AccountModel


class AccountCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = AccountModel


class AccountChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = AccountModel


class AccountAdmin(UserAdmin):
form = AccountChangeForm
add_form = AccountCreationForm


admin.site.register(AccountModel, AccountAdmin)

通过以上的简单扩展,我们就可以新建/编辑用户信息时,对新增字段进行操作了。

测试运行

将我们扩展用户模型添加到应用列表中:

1
2
3
4
5
6
# custom_user_model/settings.py

INSTALLED_APPS = [
# ...
'accounts.apps.AccountsConfig',
]

执行数据库升级,创建超级用户,最后启动开发服务器:

1
2
3
4
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

模型关联

因为自定义扩展了 Django 的用户模型,所以我们领域模型中的用户,需要以另外的方式进行关联。

1
2
3
4
5
6
7
8
from django.db import models
from custom_user_model import settings


class AccountCharge(models.Model):
account = models.ForeignKey(
settings.AUTH_USER_MODEL, # 这是新的外键关联方式!
on_delete=models.CASCADE)

通过以上几个步骤,我们就创建好了一个基于 Django 框架、适用于真实项目的基本骨架。

Django读取MySQL视图数据

Django中的业务模型有个限制:必须有ID字段。一般情况下这没有问题,但是一旦我们需要访问数据库视图数据时,这个限制就会带来麻烦。

数据库视图一般仅为全量业务数据的局部,而且通常涉及统计结果,或者多表联查结果。在这样的场景里,业务对象ID包含到视图中就缺乏实际意义了。

但是,整个应用都在Django框架下开发,且它已经提供了相当好用的ORM模块,这时候再引入其他数据库访问框架,一来应用显得更加笨重,再者增加维护成本,得不偿失。

那有没有办法解决这个矛盾呢?

有的,那就是使用row_number()函数获取记录行号作为ID,并且将模型的数据库映射置为手工管理即可,即managed = False

如果数据库使用的是MySQL 8.0之前的版本,那就需要自定义函数来模拟它。其他数据库系统都有此窗口函数。

首先创建数据库视图:

1
2
3
4
5
6
7
8
9
10
create or replace view revenue_monthly_view as
select row_number() over () as id,
vt.*
from (select sum(`t1`.`sold_price` - `t1`.`discount`) as `quantity`,
date_format(`t1`.`settled_at`, '%Y-%m') as `month`
from `product_items` `t1`
where `t1`.`status` = 'sold'
group by `month`
) vt
order by `month` desc;

接着定义好映射到数据库视图的模型:

1
2
3
4
5
6
7
8
class RevenueMonthly(models.Model):
id = models.BigAutoField('ID', primary_key=True, max_length=16)
month = models.CharField('月份', max_length=16)
quantity = models.DecimalField('数额', max_digits=16, decimal_places=2)

class Meta:
managed = False
db_table = 'revenue_monthly_view'

最后,就可以跟通常QuerySet的使用一样了。

1
2
# 获取最近12个月的收入统计
last_12_month_revenue = RevenueMonthly.objects.filter(month__gt=one_year_before)

如何使用脚本备份 MySQL 数据库?

使用 Shell 脚本对 MySQL 数据库进行备份,可以满足并发量不大的小型数据库。如果是更大数据量且并发量大的生产数据库,建议采用双(多)机热备或专业备份工具,在服务可用性的前提下,更好地保证数据的完全性和安全性。

备份用户

为数据库中单独创建一个备份用户,只赋予所需尽可能少的权限。与应用帐号隔离开。不要为了偷懒而直接使用 root 用户!

1
2
3
4
5
6
7
8
-- 创建单独的备份用户
create user 'backup'@'localhost' Identified by '复杂密码';
-- 仅赋予备份用户对 db1 下数据的查询、视图以及锁表权限
grant select,LOCK TABLES,SHOW VIEW on db1.* to 'backup'@'localhost';
-- 备份使用的 Mysqldump 还需要 reload 权限
grant reload on *.* to 'backup'@'localhost';
-- 更新到数据库中
flush privileges;

执行计划

Linux 下可使用 crontab 设定执行计划。这可能是最简单的方式了。需要注意的是,这里是在独立的系统用户下设定的。

1
2
3
4
5
# 编辑当前用户的执行计划
crontab -e

# 从早上10点到晚上10点,每2小时备份一次。
0 10-22/2 * * * ~/scripts/mysql_backup.sh

参考脚本

在当前系统用户的Home目录下新建scripts目录,创建 mysql_backup.sh 文件,根据自己的环境设定相应参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/bin/bash

# 以下配置信息请自己修改
mysql_user="backup" # MySQL备份用户
mysql_password="backup_user_password" # MySQL备份用户的密码
mysql_host="mysql_server_ip"
mysql_port="mysql_server_port"
mysql_charset="utf8mb4" # MySQL编码
backup_dbs=("db1" "db2") # 要备份的数据库名称,多个用空格分开隔开 如("db1" "db2" "db3")
backup_location=/mnt/backup/mysql # 备份数据存放位置,末尾请不要带"/",此项可以保持默认,程序会自动创建文件夹
backup_log = $backup_location/mysql_backup.log
expire_backup_delete="ON" # 是否开启过期备份删除 ON为开启 OFF为关闭
expire_days=3 # 过期时间天数 默认为三天,此项只有在expire_backup_delete开启时有效

# 本行开始以下不需要修改
backup_time=`date +%Y%m%d%H%M` # 定义备份详细时间
backup_Ymd=`date +%Y-%m-%d` # 定义备份目录中的年月日时间
backup_dir=$backup_location/$backup_Ymd # 备份文件夹全路径
welcome_msg="Welcome to use MySQL backup tools!" # 欢迎语

# 判断有没有定义备份的数据库,如果定义则开始备份,否则退出备份
if [ "$backup_dbs" == "" ];then
echo "ERROR:No database to backup! backup stop" >> $backup_log
exit
fi

# 判断MYSQL是否启动,mysql没有启动则备份退出
mysql_ps=`ps -ef |grep mysql |wc -l`
mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l`
if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then
echo "ERROR:MySQL is not running! backup stop!" >> $backup_log
exit
else
echo $welcome_msg >> $backup_log
fi

# 逐一对设定了的数据库进行备份
echo "Begining backup MySQL databases, please wait......" >> $backup_log
for dbname in ${backup_dbs[@]}
do
echo "database $dbname backup start..." >> $backup_log
`mkdir -p $backup_dir`
`mysqldump -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password $dbname --default-character-set=$mysql_charset | gzip > $backup_dir/$dbname-$backup_time.sql.gz`
flag=`echo $?`
if [ $flag == "0" ];then
echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz" >> $backup_log
else
echo "database $dbname backup fail!" >> $backup_log
fi
done

# 如果开启了删除过期备份,则进行删除操作
if [ "$expire_backup_delete" == "ON" -a "$backup_location" != "" ];then
`find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf`
echo "Expired backup data delete complete!" >> $backup_log
fi

echo "All database backup success! " >> $backup_log
exit

微信群成员可以自动批量导出来吗?

朋友问我,有什么办法可以将微信群里所有成员的昵称、微信号、所在地自动导出来呢?

找了一圈发现,微信本身没有提供这个功能,也不提供相应的开发接口。不过,既然我们可以一个个翻看微信群中的每个成员,那么这个过程可否用程序替代呢?再进一步,程序可否将微信个人信息页面的文字图像给识别出来呢?

答案是肯定的。我们可以利用UI自动化测试的相应工具,模拟用户逐一查看群成员的个人页面,截屏,再用OCR将其内容识别出来。

UI自动化测试的工具很多,适合于手机应用的有Appium和网易的Airtest,其实这俩都是跨平台的,可以测试安卓也可以测试iOS上的应用,甚至也可以担纲桌面应用的UI自动化测试。Airtest提供了开发工具、集成了pocoui和adb等,开箱即用。另外还有一点,可以用Python写测试脚本,这极大增强了测试工具的能力,而且借助强大的Python生态圈,比如Pytesseract可以帮助识别出界面文本,那我们很容易就可以将前述的几个工作步骤串联起来,形成一个完整工具了。