用flask做一个属于自己的小说书架

前言

一直想做一个属于自己的网络书架,直接读取小说txt文件,多端有阅读记录。

其他的小说网站要么要花钱,要么广告多的飞起,实在看的心累。

李辉老师那学了一周的flask,就做了这个书架,基本功能还算有了,大多都是老师教的功能实现

UI确实是有点丑陋,css都是东拼西凑合上来的,谁叫我没有艺术细胞呢?

小秋秋阅读 https://book.qiuliqi.top/

已实现的功能

1.上传txt文件自动导入小说,生成小说目录

2.注册登录后可以保存阅读记录

3.留言板功能

4.管理员账号管理会员信息和留言信息

部分代码

app.py

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
from flask import Flask, request, render_template, url_for, flash, redirect
from gevent import pywsgi
from flask_sqlalchemy import SQLAlchemy # 数据库扩展类
import os
import sys
import click
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import LoginManager, UserMixin,login_user, login_required, logout_user,current_user


WIN = sys.platform.startswith('win')
if WIN: # 如果是 Windows 系统,使用三个斜线
prefix = 'sqlite:///'
else: # 否则使用四个斜线
prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev' # 等同于 app.secret_key = 'dev'
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控
# 在扩展类实例化前加载配置
db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app
login_manager = LoginManager(app) # 实例化登录扩展类
login_manager.login_view = 'login'


# 用户表
class User(db.Model,UserMixin): # 表名将会是 user(自动生成,小写处理)
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
username = db.Column(db.String(20)) # 用户名
password_hash = db.Column(db.String(128)) # 密码散列值

def set_password(self, password): # 用来设置密码的方法,接受密码作为参数
self.password_hash = generate_password_hash(password) # 将生成的密码保持到对应字段

def validate_password(self, password): # 用于验证密码的方法,接受密码作为参数
return check_password_hash(self.password_hash, password) # 返回布尔值


# 留言表
class Movie(db.Model): # 表名将会是 movie
id = db.Column(db.Integer, primary_key=True) # 主键
content = db.Column(db.String(60)) # 留言内容
name = db.Column(db.String(6)) # 昵称


# 阅读记录表
class Read(db.Model): # 表名将会是 read
id = db.Column(db.Integer, primary_key=True) # 主键
uesrname = db.Column(db.String(20)) # 用户名
bookname = db.Column(db.String(20)) # 书名
zjid = db.Column(db.Integer) # 书名


# 初始化数据库
@app.cli.command() # 注册为命令
@click.option('--drop', is_flag=True, help='删除后创建.') # 设置选项
def initdb(drop):
"""初始化数据库."""
if drop: # 判断是否输入了选项
db.drop_all() # 删除数据库
db.create_all()
click.echo('已初始化的数据库.')


# 生成管理员账号
@app.cli.command()
@click.option('--username', prompt=True, help='The username used to login.')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.')
def admin(username, password):
"""Create user."""
db.create_all()

user = User.query.first()
if user is not None:
click.echo('Updating user...')
user.username = username
user.set_password(password) # 设置密码
else:
click.echo('Creating user...')
user = User(username=username, name='Admin')
user.set_password(password) # 设置密码
db.session.add(user)

db.session.commit() # 提交数据库会话
click.echo('Done.')


# 初始化 Flask-Login
@login_manager.user_loader
def load_user(user_id): # 创建用户加载回调函数,接受用户 ID 作为参数
user = User.query.get(int(user_id)) # 用 ID 作为 User 模型的主键查询对应的用户
return user # 返回用户对象


# 首页
@app.route('/', methods=['GET', 'POST'])
def index():
# 处理新增留言
if request.method == 'POST': # 判断是否是 POST 请求
# 获取表单数据
content = request.form.get('content') # 传入表单对应输入字段的 name 值
name = request.form.get('name')
# 验证数据
if not content or not name or len(name) > 6 or len(content) > 60:
flash('输入无效.') # 显示错误提示
return redirect(url_for('index')) # 重定向回主页
# 保存表单数据到数据库
movie = Movie(content=content, name=name) # 创建记录
db.session.add(movie) # 添加到数据库会话
db.session.commit() # 提交数据库会话
flash('您的留言已提交成功.')
return redirect(url_for('index')) # 重定向回主页
# 读取留言信息
ly_lists = Movie.query.all()
# 读取小说列表
book_lists = os.listdir('static/book')
return render_template('index.html', book_lists=book_lists, ly_lists=ly_lists)


# 登录页
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

if not username or not password:
flash('输入无效.')
return redirect(url_for('login'))

user = User.query.filter_by(username=username).first()
if user:
# 验证用户名和密码是否一致
if username == user.username and user.validate_password(password):
login_user(user) # 登入用户
flash(current_user.username + ',欢迎回家!')
return redirect(url_for('index')) # 重定向到主页

flash('无效的用户名或密码.') # 如果验证失败,显示错误消息
return redirect(url_for('login')) # 重定向回登录页面

return render_template('login.html')


# 注册页
@app.route('/signup', methods=['GET', 'POST'])
def signup():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

# 检测正确性
if not username or not password or len(username) > 20 or len(password) > 20:
flash('输入无效.')
return redirect(url_for('signup'))
# 调戏大佬
if username.lower() == 'admin':
flash('大佬,管理员是小秋秋哦!')
return redirect(url_for('signup'))
# 检查账号重复项
users = User.query.filter_by(username=username).first()
if users: # 如果查询到,则重复
flash('注册账号重复,请重新输入!')
return redirect(url_for('signup'))
else: # 没有查询到,则注册
user = User(username=username, name=username)
user.set_password(password) # 设置密码
db.session.add(user)
db.session.commit() # 提交数据库会话
flash('注册成功!')
return redirect(url_for('login')) # 前往登录页

return render_template('signup.html')


# 会员管理
@app.route('/admin')
@login_required # 用于视图保护,后面会详细介绍
def admin():
if current_user.id == 1: # 如果会员id为 1
# 查询所有会员信息
admins = User.query.all()
return render_template('admin.html', admins=admins)


# 删除会员
@app.route('/admin/delete/<int:admin_id>', methods=['POST'])
@login_required # 用于视图保护,后面会详细介绍
def admindelete(admin_id):
if admin_id != 1:
admim = User.query.get_or_404(admin_id) # 获取会员记录
name = admim.username
# 删除阅读记录
readjls = Read.query.filter_by(uesrname=name).all()
if readjls:
for readjl in readjls:
db.session.delete(readjl)
# 删除会员
db.session.delete(admim)
db.session.commit() # 提交数据库会话
flash('会员已删除!')

return redirect(url_for('admin')) # 重定向回会员页


# 登出页
@app.route('/logout')
@login_required # 用于视图保护,后面会详细介绍
def logout():
logout_user() # 登出用户
flash('欢迎下次光临!')
return redirect(url_for('index')) # 重定向回首页


# 删除留言
@app.route('/movie/delete/<int:movie_id>', methods=['POST']) # 限定只接受 POST 请求
@login_required # 用于视图保护,后面会详细介绍
def delete(movie_id):
movie = Movie.query.get_or_404(movie_id) # 获取电影记录
db.session.delete(movie) # 删除对应的记录
db.session.commit() # 提交数据库会话
flash('留言已删除.')
return redirect(url_for('index')) # 重定向回主页


# 目录页
@app.route('/book/<book_id>')
def book_page(book_id='许仙志.txt'):
book_path = 'static/book/' + book_id
with open(book_path, 'r', encoding='utf-8') as f:
txts = f.readlines()
nums = int(len(txts) / 100) + 1
page_nums = range(1, int(nums))
return render_template('page.html', book_id=book_id, page_nums=page_nums)


# 阅读记录
@app.route('/record')
@login_required # 用于视图保护,后面会详细介绍
def record():
# 数据库查找阅读记录
zj_ids = Read.query.filter_by(uesrname=current_user.username).all() # 返回包含所有查询记录的列表
return render_template('record.html', zj_ids=zj_ids)


# 阅读页
@app.route('/<book_id>/<int:movie_id>', methods=['GET', 'POST'])
def home(book_id='许仙志.txt', movie_id=1):
book_path = 'static/book/' + book_id
with open(book_path, 'r', encoding='utf-8') as f:
txts = f.readlines()
# 如果已经登录,则记录阅读记录
if current_user.is_authenticated:
username = current_user.username
if movie_id == 1: # 如果直接点阅读访问第一章,则查询阅读记录
xj_ids = Read.query.filter_by(uesrname=username, bookname=book_id).first()
if xj_ids: # 如果查询到,这读取章节id
movie_id = xj_ids.zjid
else: # 如果查询不到到,则创建记录
r1 = Read(uesrname=username, bookname=book_id, zjid=movie_id) # # 创建一个 Read 记录
db.session.add(r1) # 把新创建的记录添加到数据库会话
db.session.commit() # 提交数据库会话,只需要在最后调用一次即可
else: # 如果不是阅读第一章,则更新阅读记录
xj_ids = Read.query.filter_by(uesrname=username, bookname=book_id).first()
if xj_ids:
xj_ids.zjid = movie_id
db.session.commit() # 注意仍然需要调用这一行来提交改动
else: # 如果查询不到到,则创建记录
r1 = Read(uesrname=username, bookname=book_id, zjid=movie_id) # # 创建一个 Read 记录
db.session.add(r1) # 把新创建的记录添加到数据库会话
db.session.commit() # 提交数据库会话,只需要在最后调用一次即可

# 更新
xj_ids = Read.query.filter_by(uesrname=username, bookname=book_id).first()
xj_ids.zjid = movie_id
db.session.commit() # 注意仍然需要调用这一行来提交改动

txt_mun = movie_id * 100
return render_template('home.html', book_id=book_id, movie_id=movie_id, contents=txts[txt_mun-100:txt_mun])


@app.errorhandler(404) # 传入要处理的错误代码
def page_not_found(e): # 接受异常对象作为参数
return render_template('404.html'), 404 # 返回模板和状态码


if __name__ == '__main__':
server = pywsgi.WSGIServer(('0.0.0.0', 5000), app)
server.serve_forever()

提示 你可以在 GitHub 上查看完整项目

后记

把所有代码放在 app.py 里会让后续的开发和维护变得麻烦,但是我太懒了,觉得重构代码更麻烦,就没有使用包组织代码了…