源码

大纲

Python 远程连接 mysql 数据库后,将操作封装成函数,由 Flask 调用并渲染前端。

ER图

关系模式:

  • Newspapers(NewsID(primary key), unique key(NewsName, UpdateTime), Kind, Price, Copies)
  • Users(PhoneNum(primary key), UserName, UserPwd)
  • Subscribe(PeoID(foreign key), NewsID(foreign key), Num);

前奏

配置 mysql

搞了个百度云服务器,40元/月,安装 mysql

设置数据库密码,8.0 版本和之前操作不一样,可以看这篇文章

需要将配置文件中的相应行注释掉,以及在 mysql 数据库中将相应用户名的 host 改为 %

1
update user set host = '%' where user = '用户名'

建库

创建数据库和用户,赋予用户某数据库的管理员权限。

1
grant all privileges on Database.* to '用户名'@'%';

建表

Python 远程连接数据库后建表(表格应该在服务器上建)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
content = pymysql.Connect(
host = '', # mysql的主机ip
port = 3306, # 端口
user = user, # 用户名
passwd = pwd, # 数据库密码
db = '', # 数据库名
charset = 'utf8', # 字符集
)
print("管理员登陆成功")
return content

except:
print("管理员登陆失败")
return None

远程建表是为了方便调试。

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
# 初始化(执行一次)
def Start(content):
cur = content.cursor()

# 删除表
try:
#cur.execute("")
cur.execute("DROP TABLE Subscribe;")
cur.execute("DROP TABLE Newspapers;")
cur.execute("DROP TABLE Users;")
print("删除成功")
except:
print("删除失败")

# Newspapers
cur.execute("CREATE TABLE IF NOT EXISTS Newspapers( NewsID INT UNSIGNED AUTO_INCREMENT, NewsName CHAR(50) NOT NULL, Kind CHAR(20) NOT NULL, UpdateTime CHAR(10) NOT NULL, Price FLOAT UNSIGNED NOT NULL, Copies INT UNSIGNED NOT NULL, PRIMARY KEY ( NewsID )) CHARSET=utf8;")

print("Newspapers建立成功")

# 联合unique
cur.execute("Alter table Newspapers add UNIQUE key(NewsName,UpdateTime);")
content.commit()

# Peoples
# cur.execute(
# "CREATE TABLE IF NOT EXISTS Users(UserName CHAR(50) NOT NULL, UserPwd CHAR(50) NOT NULL, PhoneNum CHAR(11) NOT NULL UNIQUE, PRIMARY KEY (UserName)) CHARSET=utf8;")
cur.execute(
"CREATE TABLE IF NOT EXISTS Users(UserName CHAR(50) NOT NULL, UserPwd CHAR(50) NOT NULL, PhoneNum CHAR(11) NOT NULL UNIQUE, PRIMARY KEY (UserName), Email CHAR(20), Address CHAR(20)) CHARSET=utf8;")
content.commit()
print("Users建立成功")

# Records
cur.execute(
"CREATE TABLE IF NOT EXISTS Subscribe(UserName CHAR(50) NOT NULL, NewsID INT UNSIGNED, Num INT UNSIGNED NOT NULL, FOREIGN KEY(UserName) REFERENCES Users(UserName) ON DELETE CASCADE, FOREIGN KEY(NewsID) REFERENCES Newspapers(NewsID) ON DELETE CASCADE, Primary KEY(Username, NewsID)) CHARSET=utf8;")
content.commit()
print("Subscribe建立成功")

CREATE TABLE IF NOT EXISTS 语句,保证即使重复调用函数也不会报错。

同时添加一些测试函数。

1
2
3
4
5
6
7
8
9
# 测试用
# 查看所有表
def ViewTables(content):
cur = content.cursor()

cur.execute("show tables;")

# 返回游标
return cur

功能

登录

嫖了别人的UI

背景的粒子系统可以通过引入如下脚本实现。

1
2
// 粒子背景
!function(){function n(n,e,t){return n.getAttribute(e)||t}function e(n){return document.getElementsByTagName(n)}function t(){var t=e("script"),o=t.length,i=t[o-1];return{l:o,z:n(i,"zIndex",-1),o:n(i,"opacity",.5),c:n(i,"color","0,0,0"),n:n(i,"count",99)}}function o(){a=m.width=window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,c=m.height=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight}function i(){r.clearRect(0,0,a,c);var n,e,t,o,m,l;s.forEach(function(i,x){for(i.x+=i.xa,i.y+=i.ya,i.xa*=i.x>a||i.x<0?-1:1,i.ya*=i.y>c||i.y<0?-1:1,r.fillRect(i.x-.5,i.y-.5,1,1),e=x+1;e<u.length;e++)n=u[e],null!==n.x&&null!==n.y&&(o=i.x-n.x,m=i.y-n.y,l=o*o+m*m,l<n.max&&(n===y&&l>=n.max/2&&(i.x-=.03*o,i.y-=.03*m),t=(n.max-l)/n.max,r.beginPath(),r.lineWidth=t/2,r.strokeStyle="rgba("+d.c+","+(t+.2)+")",r.moveTo(i.x,i.y),r.lineTo(n.x,n.y),r.stroke()))}),x(i)}var a,c,u,m=document.createElement("canvas"),d=t(),l="c_n"+d.l,r=m.getContext("2d"),x=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(n){window.setTimeout(n,1e3/45)},w=Math.random,y={x:null,y:null,max:2e4};m.id=l,m.style.cssText="position:fixed;top:0;left:0;z-index:"+d.z+";opacity:"+d.o,e("body")[0].appendChild(m),o(),window.onresize=o,window.onmousemove=function(n){n=n||window.event,y.x=n.clientX,y.y=n.clientY},window.onmouseout=function(){y.x=null,y.y=null};for(var s=[],f=0;d.n>f;f++){var h=w()*a,g=w()*c,v=2*w()-1,p=2*w()-1;s.push({x:h,y:g,xa:v,ya:p,max:6e3})}u=s.concat([y]),setTimeout(function(){i()},100)}();

管理员登录直接固定(太懒了), session 保存。

1
2
3
if user == '' and pwd == '':
session['user_info'] = user
return redirect('/index')

用户登录前需要先读取 Users 表的 UserNameUserPwd

为了保证安全性(防止泄露 Admin ),需要创建旁观者用户访问(最好再把密码哈希一下)。

注册

注册界面和登录界面差不多。

注册界面

报刊管理

主界面

目前主要实现了添加、更新和删除功能以及一些警告功能。

更新(可以按照旧数据自动填写表单。)

删除(有二次确认提醒)

警告

由于有多个表单和按钮,因此需要不同的命名以防后端读取时发生错乱。

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
# 操作判断
if add == 'add' or update != None:
name = request.form.get('name')
price = request.form.get('price')
updateTime = request.form.get('updateTime')
kind = request.form.get('kind')

# 修改类型
name = "'" + name + "'"
kind = "'" + kind + "'"
updateTime = "'" + updateTime + "'"
price = float(price)

if(add == 'add'):
flag = Admin.AddNewspaper(content, name, kind, updateTime, price)
else:
flag = Admin.UpdateNewspaper(content, name, kind, updateTime, price, int(update))
flash(flag)

if deleteFlag == True:
ID = request.form.get('delID')
print(ID)
ID = int(ID)
flag = Admin.DelNewspaper(content, ID)
flash(flag)

由于表单是弹出的,且弹出后要用蒙版遮挡,因此要通过脚本控制他们的 display

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
function Add()
{
var mask = document.getElementById("mask");
mask.style.display = 'inherit';

var form = document.getElementById("form");
form.style.display = 'block';

var add = document.getElementById("add");
add.style.display = 'block';
var update = document.getElementById("update");
update.style.display = 'none';
}
function NotAdd()
{
var form = document.getElementById("form");
form.style.display = 'none';

var mask = document.getElementById("mask");
mask.style.display = 'none';
}

function Update()
{
var mask = document.getElementById("mask");
mask.style.display = 'inherit';

var form = document.getElementById("form");
form.style.display = 'block';

var add = document.getElementById("add");
add.style.display = 'none';
var update = document.getElementById("update");
update.style.display = 'block';

// 获取旧的
button = window.event.target;
name = button.parentElement.parentElement.children[1].innerText;
kind = button.parentElement.parentElement.children[2].innerText;
updateTime = button.parentElement.parentElement.children[3].innerText;
price = button.parentElement.parentElement.children[4].innerText;

// 返回ID
update.value = button.parentElement.parentElement.children[0].innerText;

form.children[2].children[0].children[0].children[0].value = name;
form.children[2].children[0].children[1].children[0].value = price;

switch(kind)
{
case "News":
form.children[2].children[0].children[2].children[0].checked = true;
break;
default:
form.children[2].children[0].children[2].children[1].checked = true;
break;
}

switch(updateTime)
{
case "Day":
form.children[2].children[0].children[3].children[0].checked = true;
break;
case "Mouth":
form.children[2].children[0].children[3].children[1].checked = true;
break;
case "Season":
form.children[2].children[0].children[3].children[2].checked = true;
break;
case "Year":
form.children[2].children[0].children[3].children[3].checked = true;
break;
}
}

function Delete()
{
var mask = document.getElementById("mask");
mask.style.display = 'inherit';

var sureBox = document.getElementById("sureBox");
sureBox.style.display = 'block';

var delID = document.getElementById("delID");
// 获取旧的
button = window.event.target;
// 返回ID
delID.value = button.parentElement.parentElement.children[0].innerText;
}
function CloseSureBox()
{
var sureBox = document.getElementById("sureBox");
sureBox.style.display = 'none';

var mask = document.getElementById("mask");
mask.style.display = 'none';
}

最后可以通过 flash 功能传递后端的操作状态返回给前端。

1
flash(flag)
1
2
3
{% for message in get_flashed_messages() %}
<div id=flash hidden>{{ message }}</div>
{% endfor %}
1
2
3
4
5
var flash = document.getElementById("flash");

if(flash != null)
if(flash.innerText == "False")
alert("操作失败");

用户和订阅管理

界面与报刊管理差不多。

订阅管理界面

用户管理界面

区别管理员和普通用户

可以将后端存储的登录信息传给前端。

1
2
3
4
5
context={'user_info': user_info}

...

return render_template('subscribe.html', subscribes=cur, sum = sum, **context)

再在 JS 里根据登录信息对一些组件进行隐藏。

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
if(session.innerText == "Admin")
{
var addButton = document.querySelectorAll('[id=addButton]');
for(var i = 0; i < addButton.length; i++) {
addButton[i].style.display = 'float';
}

var updateButton = document.querySelectorAll('[id=updateButton]');
for(var i = 0; i < updateButton.length; i++) {
updateButton[i].style.display = 'float';
}

var deleteButton = document.querySelectorAll('[id=deleteButton]');
for(var i = 0; i < deleteButton.length; i++) {
deleteButton[i].style.display = 'float';
}

var sum = document.getElementById("sum");
sum.style.display = 'none';
}
else
{
var addButton = document.querySelectorAll('[id=addButton]');
for(var i = 0; i < addButton.length; i++) {
addButton[i].style.display = 'none';
}

var updateButton = document.querySelectorAll('[id=updateButton]');
for(var i = 0; i < updateButton.length; i++) {
updateButton[i].style.display = 'float';
}

var deleteButton = document.querySelectorAll('[id=deleteButton]');
for(var i = 0; i < deleteButton.length; i++) {
deleteButton[i].style.display = 'float';
}

var sum = document.getElementById("sum");
sum.style.display = 'float';
}

后端也需要根据登录信息进行相应的查询,如下为查询当前用户的订阅记录。

1
2
3
4
def ViewPartSubscribe(content, userName):
cur = content.cursor()
cur.execute("select * from Subscribe where UserName={};".format(userName))
return cur

补充

顶栏

一直用的是之前从 codepen 里整理出的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class = "wrapper">
<!-- 导航栏 -->
<div class = "nav">
<div class = "logo">
<h4>报刊订阅管理系统</h4>
</div>
<div class = "links">
<a href = "newspapers" class = "mainlink">Newspapers</a>
<a href = "subscribe">Subscribe</a>
<a href = "users">Users</a>
<a href = "login">Login</a>
<a href = "logout">Logout</a>
</div>
</div>
</div>
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/* 引入字体 */
@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap");


/* 引入aos动画样式 */
@import url("https://unpkg.com/aos@2.3.1/dist/aos.css");


/* 全局设置 */
html,body{
/* background-color: #fff; */
font-family: 'Source Sans Pro', sans-serif;
overflow-x: hidden !important;
margin: 0px !important;
padding: 0px !important;
}
*{
text-decoration: none !important;
}


/* 导航栏 */
.nav{
position: fixed;
z-index: 1000;
top: 0;
right: 0;
left: 0;
height: 80px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 25px 0 25px;
background-color: #fff;
box-shadow: 0 19px 38px rgba(0,0,0,0.10);
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
/* 分支站点 */
.nav .links a{
margin-right: 30px;
font-size: 16px;
font-weight: 600;
color: #000;
}
.nav .links a:hover{
opacity: 60%;
}
.nav .links .mainlink{
margin-right: 40px;
color: #e0501b;
}
/* 左上标题 or Logo */
.nav h4{
font-size: 22px;
font-weight: bold;
margin-left: 25px;
}


/* 页脚,放网站备案 */
.footer{
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
.footerlinks a{
margin: 20px;
font-size: 16px;
font-weight: 600;
color: #000;
}
.footerlinks .mainlink{
color: #e0501b;
}


/* 手机 */
@media only screen and (max-width: 600px){

/* 导航栏 */
.nav{
flex-direction: column;
justify-content: space-around;
padding: 20px;
height: auto;
overflow-y: hidden;
}

.nav h4{
font-size: 6vw;
margin-top: 10px;
margin-bottom: 14px;
}

.nav .links a{
margin-right: 25px;
font-size: 12px;
font-weight: 600;
}

/* 页脚,放网站备案 */
.footer{
padding-bottom: 40px;
}
}