Compare commits
3 Commits
345da7ed48
...
f3ede6cb19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3ede6cb19 | ||
|
|
b0931ae38c | ||
|
|
02f2e1d972 |
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
application/__pycache__/__init__.cpython-311.pyc
|
||||||
|
logs/*.log
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
52
add_book_file.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# 设置Django环境变量
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||||
|
|
||||||
|
# 初始化Django
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from crud_book.models import CrudBookModel
|
||||||
|
from django.core.files import File
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# 获取ID为2的图书
|
||||||
|
book = CrudBookModel.objects.filter(id=2).first()
|
||||||
|
|
||||||
|
if book:
|
||||||
|
print(f'当前图书信息: {book}')
|
||||||
|
|
||||||
|
# 源文件路径
|
||||||
|
source_file = 'booksdata/Hamlet/pg1524-images.epub'
|
||||||
|
source_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), source_file)
|
||||||
|
|
||||||
|
# 检查源文件是否存在
|
||||||
|
if os.path.exists(source_path):
|
||||||
|
print(f'找到源文件: {source_path}')
|
||||||
|
|
||||||
|
# 目标路径
|
||||||
|
media_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'media')
|
||||||
|
books_dir = os.path.join(media_root, 'books')
|
||||||
|
os.makedirs(books_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 复制文件到媒体目录
|
||||||
|
target_filename = 'hamlet.epub'
|
||||||
|
target_path = os.path.join(books_dir, target_filename)
|
||||||
|
shutil.copy2(source_path, target_path)
|
||||||
|
print(f'文件已复制到: {target_path}')
|
||||||
|
|
||||||
|
# 更新图书的file字段
|
||||||
|
with open(target_path, 'rb') as f:
|
||||||
|
book.file.save(target_filename, File(f), save=True)
|
||||||
|
|
||||||
|
print(f'图书文件已更新: {book.file.url}')
|
||||||
|
print(f'文件路径: {book.file.path}')
|
||||||
|
else:
|
||||||
|
print(f'源文件不存在: {source_path}')
|
||||||
|
else:
|
||||||
|
print('未找到ID为2的图书')
|
||||||
BIN
application/__pycache__/asgi.cpython-311.pyc
Normal file
@ -5,7 +5,7 @@ from django.db import connection
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from dvadmin.utils.validator import CustomValidationError
|
from dvadmin.utils.validator import CustomValidationError
|
||||||
|
|
||||||
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
|
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'redis') # redis
|
||||||
|
|
||||||
|
|
||||||
def is_tenants_mode():
|
def is_tenants_mode():
|
||||||
|
|||||||
@ -70,12 +70,12 @@ My_Apps = [
|
|||||||
INSTALLED_APPS += My_Apps
|
INSTALLED_APPS += My_Apps
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
"corsheaders.middleware.CorsMiddleware", # 跨域中间件
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
"dvadmin.utils.middleware.HealthCheckMiddleware",
|
"dvadmin.utils.middleware.HealthCheckMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"corsheaders.middleware.CorsMiddleware", # 跨域中间件
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
@ -162,6 +162,7 @@ STATICFILES_DIRS = [
|
|||||||
MEDIA_ROOT = "media" # 项目下的目录
|
MEDIA_ROOT = "media" # 项目下的目录
|
||||||
MEDIA_URL = "/media/" # 跟STATIC_URL类似,指定用户可以通过这个url找到文件
|
MEDIA_URL = "/media/" # 跟STATIC_URL类似,指定用户可以通过这个url找到文件
|
||||||
|
|
||||||
|
|
||||||
#添加以下代码以后就不用写{% load staticfiles %},可以直接引用
|
#添加以下代码以后就不用写{% load staticfiles %},可以直接引用
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
@ -171,12 +172,41 @@ STATICFILES_FINDERS = (
|
|||||||
# python manage.py collectstatic
|
# python manage.py collectstatic
|
||||||
# STATIC_ROOT=os.path.join(BASE_DIR,'static')
|
# STATIC_ROOT=os.path.join(BASE_DIR,'static')
|
||||||
|
|
||||||
|
# ================================================= #
|
||||||
|
# ******************* Redis缓存配置 ******************* #
|
||||||
|
# ================================================= #
|
||||||
|
# 根据是否有密码决定Redis URL格式
|
||||||
|
if REDIS_PASSWORD:
|
||||||
|
REDIS_CACHE_URL = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:6379/{REDIS_DB}"
|
||||||
|
else:
|
||||||
|
REDIS_CACHE_URL = f"redis://{REDIS_HOST}:6379/{REDIS_DB}"
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": REDIS_CACHE_URL,
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
"CONNECTION_POOL_KWARGS": {
|
||||||
|
"max_connections": 100,
|
||||||
|
"decode_responses": True
|
||||||
|
},
|
||||||
|
"SOCKET_CONNECT_TIMEOUT": 5, # 连接超时时间(秒)
|
||||||
|
"SOCKET_TIMEOUT": 5, # 读写超时时间(秒)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
# ******************* 跨域的配置 ******************* #
|
# ******************* 跨域的配置 ******************* #
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
|
|
||||||
# 全部允许配置
|
# 全部允许配置
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
]
|
||||||
# 允许cookie
|
# 允许cookie
|
||||||
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作
|
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作
|
||||||
|
|
||||||
@ -184,19 +214,21 @@ CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持
|
|||||||
# ********************* channels配置 ******************* #
|
# ********************* channels配置 ******************* #
|
||||||
# ===================================================== #
|
# ===================================================== #
|
||||||
ASGI_APPLICATION = 'application.asgi.application'
|
ASGI_APPLICATION = 'application.asgi.application'
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "channels.layers.InMemoryChannelLayer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# CHANNEL_LAYERS = {
|
# CHANNEL_LAYERS = {
|
||||||
# 'default': {
|
# "default": {
|
||||||
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
# "BACKEND": "channels.layers.InMemoryChannelLayer"
|
||||||
# 'CONFIG': {
|
# }
|
||||||
# "hosts": [('127.0.0.1', 6379)], #需修改
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# }
|
# }
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||||
|
'CONFIG': {
|
||||||
|
"hosts": [(f'{REDIS_HOST}', 6379)],
|
||||||
|
"password": REDIS_PASSWORD if REDIS_PASSWORD else None,
|
||||||
|
"db": REDIS_DB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
@ -386,6 +418,19 @@ API_MODEL_MAP = {
|
|||||||
|
|
||||||
DJANGO_CELERY_BEAT_TZ_AWARE = False
|
DJANGO_CELERY_BEAT_TZ_AWARE = False
|
||||||
CELERY_TIMEZONE = "Asia/Shanghai" # celery 时区问题
|
CELERY_TIMEZONE = "Asia/Shanghai" # celery 时区问题
|
||||||
|
# Celery配置 - 使用Redis作为broker和result backend
|
||||||
|
if REDIS_PASSWORD:
|
||||||
|
CELERY_BROKER_URL = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:6379/{CELERY_BROKER_DB}"
|
||||||
|
CELERY_RESULT_BACKEND = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:6379/{REDIS_DB}"
|
||||||
|
else:
|
||||||
|
CELERY_BROKER_URL = f"redis://{REDIS_HOST}:6379/{CELERY_BROKER_DB}"
|
||||||
|
CELERY_RESULT_BACKEND = f"redis://{REDIS_HOST}:6379/{REDIS_DB}"
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
|
CELERY_TASK_TIME_LIMIT = 3600 # 任务最长执行时间(秒)
|
||||||
|
CELERY_TASK_SOFT_TIME_LIMIT = 3000 # 任务软超时时间(秒)
|
||||||
|
CELERY_TASK_TRACK_STARTED = True # 跟踪任务开始时间
|
||||||
# 静态页面压缩
|
# 静态页面压缩
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
|
||||||
|
|
||||||
@ -423,3 +468,6 @@ from dvadmin3_celery.settings import * # celery 异步任务
|
|||||||
#from dvadmin_uniapp.settings import *
|
#from dvadmin_uniapp.settings import *
|
||||||
# ...
|
# ...
|
||||||
# ********** 一键导入插件配置结束 **********
|
# ********** 一键导入插件配置结束 **********
|
||||||
|
|
||||||
|
# api地址
|
||||||
|
API_URL = 'http://127.0.0.1:8000/'
|
||||||
BIN
booksdata/Pride and Prejudice/pg1342-images-3.epub
Normal file
BIN
booksdata/Pride and Prejudice/pg1342.cover.medium.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
29
check_book_file.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# 设置Django环境变量
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||||
|
|
||||||
|
# 初始化Django
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# 现在可以导入模型
|
||||||
|
from crud_book.models import CrudBookModel
|
||||||
|
|
||||||
|
# 查询ID为2的图书
|
||||||
|
book = CrudBookModel.objects.filter(id=2).first()
|
||||||
|
print(f'Book exists: {book is not None}')
|
||||||
|
|
||||||
|
if book:
|
||||||
|
print(f'File exists: {bool(book.file)}')
|
||||||
|
if book.file:
|
||||||
|
try:
|
||||||
|
print(f'File path: {book.file.path}')
|
||||||
|
print(f'File exists on disk: {os.path.exists(book.file.path)}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error accessing file path: {e}')
|
||||||
|
print(f'File url: {book.file.url if book.file else None}')
|
||||||
@ -30,9 +30,13 @@ TABLE_PREFIX = "dvadmin_"
|
|||||||
# ================================================= #
|
# ================================================= #
|
||||||
REDIS_DB = 1
|
REDIS_DB = 1
|
||||||
CELERY_BROKER_DB = 3
|
CELERY_BROKER_DB = 3
|
||||||
REDIS_PASSWORD = 'DVADMIN3'
|
REDIS_PASSWORD = '' # 无密码的Redis服务器
|
||||||
REDIS_HOST = '127.0.0.1'
|
REDIS_HOST = '127.0.0.1'
|
||||||
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
|
# 根据是否有密码生成正确的Redis URL
|
||||||
|
if REDIS_PASSWORD:
|
||||||
|
REDIS_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:6379'
|
||||||
|
else:
|
||||||
|
REDIS_URL = f'redis://{REDIS_HOST}:6379'
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
# ****************** 功能 启停 ******************* #
|
# ****************** 功能 启停 ******************* #
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
|
|||||||
58
crud_book/README_MIGRATION.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 数据库迁移说明
|
||||||
|
|
||||||
|
## 新增字段和模型
|
||||||
|
|
||||||
|
本次更新为图书管理系统添加了以下功能:
|
||||||
|
|
||||||
|
### 1. CrudBookModel 新增字段
|
||||||
|
- `file`: 电子书文件字段(FileField)
|
||||||
|
- `file_type`: 文件类型字段(CharField),用于标识文件格式(epub, pdf, mobi等)
|
||||||
|
|
||||||
|
### 2. 新增 ReadingProgress 模型
|
||||||
|
用于记录用户的阅读进度,包含以下字段:
|
||||||
|
- `user`: 用户外键
|
||||||
|
- `book`: 图书外键
|
||||||
|
- `location`: 阅读位置(EPUB CFI 或其他位置标识)
|
||||||
|
- `progress`: 阅读进度百分比(0-100)
|
||||||
|
|
||||||
|
## 执行迁移
|
||||||
|
|
||||||
|
在后端项目目录下执行以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成迁移文件
|
||||||
|
python manage.py makemigrations crud_book
|
||||||
|
|
||||||
|
# 执行迁移
|
||||||
|
python manage.py migrate crud_book
|
||||||
|
```
|
||||||
|
|
||||||
|
## 新增API接口
|
||||||
|
|
||||||
|
### 1. 获取图书文件
|
||||||
|
```
|
||||||
|
GET /api/CrudBookModelViewSet/{id}/file/
|
||||||
|
```
|
||||||
|
返回图书的电子文件URL和文件类型
|
||||||
|
|
||||||
|
### 2. 保存阅读进度
|
||||||
|
```
|
||||||
|
POST /api/reading-progress/
|
||||||
|
{
|
||||||
|
"book_id": 1,
|
||||||
|
"location": "epubcfi(...)",
|
||||||
|
"progress": 45.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 获取阅读进度
|
||||||
|
```
|
||||||
|
GET /api/reading-progress/{book_id}/
|
||||||
|
```
|
||||||
|
返回指定图书的阅读进度
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保在 Django settings.py 中配置了 MEDIA_ROOT 和 MEDIA_URL
|
||||||
|
2. 需要配置静态文件服务以访问上传的电子书文件
|
||||||
|
3. 阅读进度按用户和图书唯一,同一用户同一本书只有一条进度记录
|
||||||
18
crud_book/migrations/0004_alter_crudbookmodel_image.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-21 00:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0003_alter_crudbookmodel_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='封面'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-20 22:37
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('crud_book', '0003_alter_crudbookmodel_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='books/', verbose_name='电子书文件'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='file_type',
|
||||||
|
field=models.CharField(blank=True, help_text='epub, pdf, mobi等', max_length=50, null=True, verbose_name='文件类型'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-20 23:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0004_crudbookmodel_file_crudbookmodel_file_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='file',
|
||||||
|
field=models.CharField(blank=True, help_text='电子书文件URL', max_length=500, null=True, verbose_name='电子书文件'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(blank=True, help_text='封面图片URL', max_length=500, null=True, verbose_name='封面'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
crud_book/migrations/0005_alter_crudbookmodel_image.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-21 00:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0004_alter_crudbookmodel_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, max_length=255, null=True, upload_to='', verbose_name='封面'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
crud_book/migrations/0006_alter_crudbookmodel_image.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-21 00:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0005_alter_crudbookmodel_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='封面'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
crud_book/migrations/0007_alter_crudbookmodel_sub_title.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-22 01:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0006_alter_crudbookmodel_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='sub_title',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='副标题'),
|
||||||
|
),
|
||||||
|
]
|
||||||
37
crud_book/migrations/0008_readingprogress.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Generated manually for ReadingProgress model
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('crud_book', '0007_alter_crudbookmodel_sub_title'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ReadingProgress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('description', models.CharField(blank=True, max_length=255, null=True, verbose_name='描述')),
|
||||||
|
('creator', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('dept_belong_id', models.CharField(blank=True, max_length=64, null=True, verbose_name='数据归属部门')),
|
||||||
|
('update_datetime', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')),
|
||||||
|
('create_datetime', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')),
|
||||||
|
('location', models.TextField(blank=True, null=True, verbose_name='阅读位置(CFI)')),
|
||||||
|
('progress', models.FloatField(default=0, verbose_name='阅读进度(%)')),
|
||||||
|
('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crud_book.crudbookmodel', verbose_name='图书')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '阅读进度',
|
||||||
|
'verbose_name_plural': '阅读进度',
|
||||||
|
'db_table': 'reading_progress',
|
||||||
|
'ordering': ('-update_datetime',),
|
||||||
|
'unique_together': {('user', 'book')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
14
crud_book/migrations/0009_merge_20251023_0014.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-23 00:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crud_book', '0005_alter_crudbookmodel_file_alter_crudbookmodel_image'),
|
||||||
|
('crud_book', '0008_readingprogress'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2025-10-23 00:52
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('crud_book', '0009_merge_20251023_0014'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='modifier',
|
||||||
|
field=models.CharField(blank=True, help_text='修改人', max_length=255, null=True, verbose_name='修改人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='books/', verbose_name='电子书文件'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='file_type',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='文件类型'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='crudbookmodel',
|
||||||
|
name='image',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='封面'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='creator',
|
||||||
|
field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_reading_progresses', related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='dept_belong_id',
|
||||||
|
field=models.CharField(blank=True, help_text='数据归属部门', max_length=255, null=True, verbose_name='数据归属部门'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='readingprogress',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reading_progresses', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
from dvadmin.utils.models import CoreModel
|
from dvadmin.utils.models import CoreModel
|
||||||
@ -6,7 +7,7 @@ from dvadmin.utils.models import CoreModel
|
|||||||
|
|
||||||
class CrudBookModel(CoreModel):
|
class CrudBookModel(CoreModel):
|
||||||
title = models.CharField(max_length=255, verbose_name="书名")
|
title = models.CharField(max_length=255, verbose_name="书名")
|
||||||
sub_title = models.CharField(max_length=255, verbose_name="副标题")
|
sub_title = models.CharField(max_length=255, verbose_name="副标题", null=True, blank=True)
|
||||||
series = models.CharField(max_length=255, verbose_name="系列丛书", null=True, blank=True)
|
series = models.CharField(max_length=255, verbose_name="系列丛书", null=True, blank=True)
|
||||||
author = models.CharField(max_length=255, verbose_name="作者")
|
author = models.CharField(max_length=255, verbose_name="作者")
|
||||||
translator = models.CharField(max_length=255, verbose_name="译者", null=True, blank=True)
|
translator = models.CharField(max_length=255, verbose_name="译者", null=True, blank=True)
|
||||||
@ -20,11 +21,48 @@ class CrudBookModel(CoreModel):
|
|||||||
# create_by = models.IntegerField(verbose_name="创建者编号", default=1)
|
# create_by = models.IntegerField(verbose_name="创建者编号", default=1)
|
||||||
# update_time = models.DateField(verbose_name="更新时间", null=True, blank=True)
|
# update_time = models.DateField(verbose_name="更新时间", null=True, blank=True)
|
||||||
# update_by = models.IntegerField(verbose_name="更新用户编号", null=True, blank=True)
|
# update_by = models.IntegerField(verbose_name="更新用户编号", null=True, blank=True)
|
||||||
image = models.ImageField(verbose_name="封面", null=True, blank=True)
|
# image = models.ImageField(verbose_name="封面", null=True, blank=True)
|
||||||
|
image = models.CharField(max_length=255,verbose_name="封面", null=True, blank=True)
|
||||||
location = models.CharField(max_length=255, verbose_name="存放位置", null=True, blank=True)
|
location = models.CharField(max_length=255, verbose_name="存放位置", null=True, blank=True)
|
||||||
|
file = models.FileField(upload_to='books/', verbose_name="电子书文件", null=True, blank=True)
|
||||||
|
file_type = models.CharField(max_length=50, verbose_name="文件类型", null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "books"
|
db_table = "books"
|
||||||
verbose_name = '图书表'
|
verbose_name = '图书表'
|
||||||
verbose_name_plural = verbose_name
|
verbose_name_plural = verbose_name
|
||||||
ordering = ('-create_datetime',)
|
ordering = ('-create_datetime',)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadingProgress(CoreModel):
|
||||||
|
"""阅读进度模型"""
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="用户",
|
||||||
|
related_name='reading_progresses'
|
||||||
|
)
|
||||||
|
creator = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
db_constraint=False,
|
||||||
|
help_text='创建人',
|
||||||
|
null=True,
|
||||||
|
on_delete=models.deletion.SET_NULL,
|
||||||
|
related_name='created_reading_progresses',
|
||||||
|
related_query_name='creator_query',
|
||||||
|
verbose_name='创建人'
|
||||||
|
)
|
||||||
|
book = models.ForeignKey(
|
||||||
|
CrudBookModel,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name="图书"
|
||||||
|
)
|
||||||
|
location = models.TextField(verbose_name="阅读位置(CFI)", null=True, blank=True)
|
||||||
|
progress = models.FloatField(verbose_name="阅读进度(%)", default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "reading_progress"
|
||||||
|
verbose_name = '阅读进度'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
unique_together = ('user', 'book')
|
||||||
|
ordering = ('-update_datetime',)
|
||||||
@ -1,13 +1,44 @@
|
|||||||
#backend/crud_demo/serializers.py
|
#backend/crud_demo/serializers.py
|
||||||
|
|
||||||
from crud_book.models import CrudBookModel
|
from crud_book.models import CrudBookModel, ReadingProgress
|
||||||
from dvadmin.utils.serializers import CustomModelSerializer
|
from dvadmin.utils.serializers import CustomModelSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class CrudBookModelSerializer(CustomModelSerializer):
|
class CrudBookModelSerializer(CustomModelSerializer):
|
||||||
"""
|
"""
|
||||||
序列化器
|
序列化器
|
||||||
"""
|
"""
|
||||||
|
# 添加SerializerMethodField来处理image字段
|
||||||
|
image = serializers.SerializerMethodField()
|
||||||
|
location = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_image(self, obj):
|
||||||
|
"""
|
||||||
|
获取完整的图片URL
|
||||||
|
"""
|
||||||
|
if obj.image:
|
||||||
|
# 检查image是否已经是完整的URL
|
||||||
|
if obj.image.startswith(('http://', 'https://')):
|
||||||
|
return obj.image
|
||||||
|
# 否则添加MEDIA_URL前缀
|
||||||
|
return settings.API_URL + obj.image
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_location(self, obj):
|
||||||
|
"""
|
||||||
|
获取完整的图书URL
|
||||||
|
"""
|
||||||
|
if obj.location:
|
||||||
|
# 检查location是否已经是完整的URL
|
||||||
|
if obj.location.startswith(('http://', 'https://')):
|
||||||
|
return obj.location
|
||||||
|
# 否则添加MEDIA_URL前缀
|
||||||
|
return settings.API_URL + obj.location
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
#这里是进行了序列化模型及所有的字段
|
#这里是进行了序列化模型及所有的字段
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CrudBookModel
|
model = CrudBookModel
|
||||||
@ -21,4 +52,49 @@ class CrudBookModelCreateUpdateSerializer(CustomModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CrudBookModel
|
model = CrudBookModel
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class ReadingProgressSerializer(CustomModelSerializer):
|
||||||
|
"""
|
||||||
|
阅读进度序列化器
|
||||||
|
"""
|
||||||
|
# 添加book_id字段,便于前端传递和接收
|
||||||
|
book_id = serializers.IntegerField(write_only=True, required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReadingProgress
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ('user', 'book') # 将book设为只读,使用book_id进行设置
|
||||||
|
|
||||||
|
def validate_book_id(self, value):
|
||||||
|
"""验证book_id对应的图书是否存在"""
|
||||||
|
try:
|
||||||
|
CrudBookModel.objects.get(id=value)
|
||||||
|
return value
|
||||||
|
except CrudBookModel.DoesNotExist:
|
||||||
|
raise serializers.ValidationError(f'ID为{value}的图书不存在')
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""创建阅读进度记录"""
|
||||||
|
# 获取book_id并删除,因为我们需要用它查找book对象
|
||||||
|
book_id = validated_data.pop('book_id')
|
||||||
|
|
||||||
|
# 查找对应的book对象
|
||||||
|
book = CrudBookModel.objects.get(id=book_id)
|
||||||
|
|
||||||
|
# 创建记录,设置user和book
|
||||||
|
validated_data['user'] = self.context['request'].user
|
||||||
|
validated_data['book'] = book
|
||||||
|
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""更新阅读进度记录"""
|
||||||
|
# 如果提供了book_id,则验证并更新book
|
||||||
|
if 'book_id' in validated_data:
|
||||||
|
book_id = validated_data.pop('book_id')
|
||||||
|
book = CrudBookModel.objects.get(id=book_id)
|
||||||
|
validated_data['book'] = book
|
||||||
|
|
||||||
|
return super().update(instance, validated_data)
|
||||||
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from rest_framework.routers import SimpleRouter
|
from rest_framework.routers import SimpleRouter
|
||||||
|
|
||||||
from .views import CrudBookModelViewSet
|
from .views import CrudBookModelViewSet, ReadingProgressViewSet
|
||||||
|
|
||||||
router = SimpleRouter()
|
router = SimpleRouter()
|
||||||
# 这里进行注册路径,并把视图关联上,这里的api地址以视图名称为后缀,这样方便记忆api/CrudBookModelViewSet
|
# 这里进行注册路径,并把视图关联上,这里的api地址以视图名称为后缀,这样方便记忆api/CrudBookModelViewSet
|
||||||
router.register("api/CrudBookModelViewSet", CrudBookModelViewSet)
|
router.register("api/CrudBookModelViewSet", CrudBookModelViewSet)
|
||||||
|
router.register("api/reading-progress", ReadingProgressViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
# Create your views here.
|
# Create your views here.
|
||||||
from crud_book.models import CrudBookModel
|
import os
|
||||||
from crud_book.serializers import CrudBookModelSerializer, CrudBookModelCreateUpdateSerializer
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, viewsets, serializers
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from crud_book.models import CrudBookModel, ReadingProgress
|
||||||
|
from crud_book.serializers import (
|
||||||
|
CrudBookModelSerializer,
|
||||||
|
CrudBookModelCreateUpdateSerializer,
|
||||||
|
ReadingProgressSerializer
|
||||||
|
)
|
||||||
from dvadmin.utils.viewset import CustomModelViewSet
|
from dvadmin.utils.viewset import CustomModelViewSet
|
||||||
|
|
||||||
|
|
||||||
@ -15,4 +24,153 @@ class CrudBookModelViewSet(CustomModelViewSet):
|
|||||||
queryset = CrudBookModel.objects.all()
|
queryset = CrudBookModel.objects.all()
|
||||||
serializer_class = CrudBookModelSerializer
|
serializer_class = CrudBookModelSerializer
|
||||||
create_serializer_class = CrudBookModelCreateUpdateSerializer
|
create_serializer_class = CrudBookModelCreateUpdateSerializer
|
||||||
update_serializer_class = CrudBookModelCreateUpdateSerializer
|
update_serializer_class = CrudBookModelCreateUpdateSerializer
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get'])
|
||||||
|
def file(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
获取图书文件信息或直接下载文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 先检查图书是否存在
|
||||||
|
try:
|
||||||
|
book = self.get_object()
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'code': 404, 'msg': f'图书不存在: {str(e)}'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查图书是否有文件
|
||||||
|
if not book.file:
|
||||||
|
return Response(
|
||||||
|
{'code': 404, 'msg': '该图书没有上传文件'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查文件是否实际存在于文件系统
|
||||||
|
if not os.path.exists(book.file.path):
|
||||||
|
return Response(
|
||||||
|
{'code': 404, 'msg': '文件在服务器上不存在'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回文件URL信息作为标准格式
|
||||||
|
file_url = request.build_absolute_uri(book.file.url)
|
||||||
|
return Response({
|
||||||
|
'code': 2000,
|
||||||
|
'data': {
|
||||||
|
'file_url': file_url,
|
||||||
|
'file_type': book.file_type or 'epub',
|
||||||
|
'filename': os.path.basename(book.file.name)
|
||||||
|
},
|
||||||
|
'msg': '成功'
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'msg': f'服务器错误: {str(e)}'},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadingProgressViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
阅读进度管理
|
||||||
|
"""
|
||||||
|
queryset = ReadingProgress.objects.all()
|
||||||
|
serializer_class = ReadingProgressSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""只返回当前用户的阅读进度"""
|
||||||
|
return ReadingProgress.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""创建或更新阅读进度"""
|
||||||
|
# 检查用户是否已认证
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return Response(
|
||||||
|
{'code': 401, 'msg': '用户未登录,无法保存阅读进度'},
|
||||||
|
status=status.HTTP_401_UNAUTHORIZED
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取序列化器
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 验证数据
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
# 尝试查找现有记录
|
||||||
|
try:
|
||||||
|
book_id = request.data.get('book_id')
|
||||||
|
book = CrudBookModel.objects.get(id=book_id)
|
||||||
|
|
||||||
|
# 使用update_or_create而不是普通的save,避免重复记录
|
||||||
|
progress, created = ReadingProgress.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
book=book
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
progress.location = request.data.get('location', '')
|
||||||
|
progress.progress = request.data.get('progress', 0)
|
||||||
|
progress.creator = request.user # 确保设置creator
|
||||||
|
progress.save()
|
||||||
|
|
||||||
|
# 返回更新后的序列化数据,包装成前端期望的格式
|
||||||
|
serializer = self.get_serializer(progress)
|
||||||
|
return Response(
|
||||||
|
{'code': 2000, 'data': serializer.data, 'msg': '成功'},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
except CrudBookModel.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{'code': 404, 'msg': f'ID为{book_id}的图书不存在'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
except serializers.ValidationError as e:
|
||||||
|
# 返回序列化验证错误
|
||||||
|
return Response(
|
||||||
|
{'code': 400, 'msg': str(e.detail)},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 记录错误
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f'保存阅读进度失败: {str(e)}')
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'msg': f'保存阅读进度失败: {str(e)}'},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def by_book(self, request):
|
||||||
|
"""根据图书ID获取阅读进度"""
|
||||||
|
book_id = request.query_params.get('book_id')
|
||||||
|
if not book_id:
|
||||||
|
return Response(
|
||||||
|
{'code': 400, 'msg': '缺少book_id参数'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
progress = ReadingProgress.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
book_id=book_id
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(progress)
|
||||||
|
return Response(
|
||||||
|
{'code': 2000, 'data': serializer.data, 'msg': '成功'},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
except ReadingProgress.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{'code': 2000, 'data': {'location': '', 'progress': 0}, 'msg': '未找到阅读进度记录'},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
BIN
db.sqlite3
91
fix_file_paths.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
修复数据库中文件路径的反斜杠问题
|
||||||
|
将所有 Windows 风格的反斜杠路径转换为正斜杠
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
# 设置 Django 环境
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from dvadmin.system.models import FileList
|
||||||
|
from crud_book.models import CrudBookModel
|
||||||
|
|
||||||
|
def fix_file_list_paths():
|
||||||
|
"""修复 FileList 表中的路径"""
|
||||||
|
print("开始修复 FileList 表中的路径...")
|
||||||
|
|
||||||
|
# 修复 url 字段
|
||||||
|
files_with_backslash = FileList.objects.filter(url__contains='\\')
|
||||||
|
count = 0
|
||||||
|
for file_obj in files_with_backslash:
|
||||||
|
old_url = str(file_obj.url)
|
||||||
|
new_url = old_url.replace('\\', '/')
|
||||||
|
file_obj.url = new_url
|
||||||
|
file_obj.save(update_fields=['url'])
|
||||||
|
count += 1
|
||||||
|
print(f" 修复: {old_url} -> {new_url}")
|
||||||
|
|
||||||
|
print(f"FileList.url: 共修复 {count} 条记录")
|
||||||
|
|
||||||
|
# 修复 file_url 字段
|
||||||
|
files_with_backslash = FileList.objects.filter(file_url__contains='\\')
|
||||||
|
count = 0
|
||||||
|
for file_obj in files_with_backslash:
|
||||||
|
old_url = file_obj.file_url
|
||||||
|
new_url = old_url.replace('\\', '/')
|
||||||
|
file_obj.file_url = new_url
|
||||||
|
file_obj.save(update_fields=['file_url'])
|
||||||
|
count += 1
|
||||||
|
print(f" 修复: {old_url} -> {new_url}")
|
||||||
|
|
||||||
|
print(f"FileList.file_url: 共修复 {count} 条记录")
|
||||||
|
|
||||||
|
def fix_book_paths():
|
||||||
|
"""修复图书表中的路径"""
|
||||||
|
print("\n开始修复图书表中的路径...")
|
||||||
|
|
||||||
|
# 修复 image 字段
|
||||||
|
books_with_backslash = CrudBookModel.objects.filter(image__contains='\\')
|
||||||
|
count = 0
|
||||||
|
for book in books_with_backslash:
|
||||||
|
old_path = book.image
|
||||||
|
new_path = old_path.replace('\\', '/')
|
||||||
|
book.image = new_path
|
||||||
|
book.save(update_fields=['image'])
|
||||||
|
count += 1
|
||||||
|
print(f" 修复封面: {book.title} - {old_path} -> {new_path}")
|
||||||
|
|
||||||
|
print(f"CrudBookModel.image: 共修复 {count} 条记录")
|
||||||
|
|
||||||
|
# 修复 file 字段
|
||||||
|
books_with_backslash = CrudBookModel.objects.filter(file__contains='\\')
|
||||||
|
count = 0
|
||||||
|
for book in books_with_backslash:
|
||||||
|
old_path = book.file
|
||||||
|
new_path = old_path.replace('\\', '/')
|
||||||
|
book.file = new_path
|
||||||
|
book.save(update_fields=['file'])
|
||||||
|
count += 1
|
||||||
|
print(f" 修复文件: {book.title} - {old_path} -> {new_path}")
|
||||||
|
|
||||||
|
print(f"CrudBookModel.file: 共修复 {count} 条记录")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("=" * 60)
|
||||||
|
print("开始修复文件路径中的反斜杠问题")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fix_file_list_paths()
|
||||||
|
fix_book_paths()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ 所有路径修复完成!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 修复过程中出现错误: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
164223
logs/error.log
170195
logs/server.log
BIN
media/books/hamlet.epub
Normal file
BIN
media/books/hamlet_NkSTa6g.epub
Normal file
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_BQjPNA6.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_Ce4tEKC.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_IZphLO8.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_J77rmPP.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_KHp4QoK.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_kAD3kT5.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_m3Sw3GT.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/2/a/2ae854214abcfcc08511cfdeb24a7f7a_sVNn4dM.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_0Tv6n1C.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_2FYkGTy.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_79N70vy.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_AbmV54S.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_IjZ2Hoc.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_JqCB6YW.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_LEb3Tqv.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_QDpCKFe.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_STp1xAz.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_SblsHlv.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_V4mQxVB.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_XacSCxJ.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_mfAXxM2.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_rL4AfZ4.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_uEGuuxQ.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_x9kMmGY.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/9/3/937afec48ee65d18f9369e775db632eb_yCtOYIR.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_EaBHR4o.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_V0oWJLK.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_WS07ksq.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_ada9yL1.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_cIyj9gQ.epub
Normal file
BIN
media/files/e/8/e8cc692135fd445f18dc128c36e49bff_fNC0hBr.epub
Normal file
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_8p3s2Co.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_cpEBDd8.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_djar20Y.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_dwpY1JQ.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_e8qasBE.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/e/9/e9b1cd30ddf7545a8731cf3d64a742f9_mXreRAL.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
media/files/f/6/f6998313b51c8ad90447a7b89130894f.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
@ -9,6 +9,7 @@ django-simple-captcha==0.6.0
|
|||||||
django-timezone-field==7.0
|
django-timezone-field==7.0
|
||||||
djangorestframework_simplejwt==5.4.0
|
djangorestframework_simplejwt==5.4.0
|
||||||
drf-yasg==1.21.7
|
drf-yasg==1.21.7
|
||||||
|
django-redis==5.4.0
|
||||||
|
|
||||||
pypinyin==0.51.0
|
pypinyin==0.51.0
|
||||||
ua-parser==0.18.0
|
ua-parser==0.18.0
|
||||||
|
|||||||
171
在线阅读功能部署说明.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# 在线阅读功能部署说明
|
||||||
|
|
||||||
|
## 后端部署步骤
|
||||||
|
|
||||||
|
### 1. 数据库迁移
|
||||||
|
|
||||||
|
在线阅读功能添加了以下数据库变更:
|
||||||
|
|
||||||
|
**CrudBookModel 模型新增字段:**
|
||||||
|
- `file`: 电子书文件字段(FileField)
|
||||||
|
- `file_type`: 文件类型字段(CharField)
|
||||||
|
|
||||||
|
**新增 ReadingProgress 模型:**
|
||||||
|
- `user`: 用户外键
|
||||||
|
- `book`: 图书外键
|
||||||
|
- `location`: 阅读位置(CFI格式)
|
||||||
|
- `progress`: 阅读进度百分比
|
||||||
|
|
||||||
|
**执行迁移命令:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd django-vue3-admin-backend
|
||||||
|
|
||||||
|
# 创建迁移文件
|
||||||
|
python manage.py makemigrations crud_book
|
||||||
|
|
||||||
|
# 执行迁移
|
||||||
|
python manage.py migrate crud_book
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 新增API接口
|
||||||
|
|
||||||
|
**图书文件接口:**
|
||||||
|
- `GET /api/CrudBookModelViewSet/{id}/file/` - 获取图书文件信息
|
||||||
|
|
||||||
|
**阅读进度接口:**
|
||||||
|
- `POST /api/reading-progress/` - 保存/更新阅读进度
|
||||||
|
- `GET /api/reading-progress/by_book/?book_id={id}` - 获取指定图书的阅读进度
|
||||||
|
- `GET /api/reading-progress/` - 获取当前用户所有阅读进度
|
||||||
|
|
||||||
|
### 3. 配置检查
|
||||||
|
|
||||||
|
确保 `settings.py` 中已配置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 媒体文件配置
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
```
|
||||||
|
|
||||||
|
确保主 `urls.py` 中已配置静态文件服务:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# ... 其他路由
|
||||||
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端部署步骤
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd django-vue3-admin-web
|
||||||
|
|
||||||
|
# 使用yarn安装(推荐)
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# 或使用npm
|
||||||
|
npm install --legacy-peer-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置检查
|
||||||
|
|
||||||
|
已添加的文件:
|
||||||
|
- `src/views/book/reader/index.vue` - 阅读器组件
|
||||||
|
- `src/api/book/reader.ts` - API接口定义
|
||||||
|
- `src/router/route.ts` - 路由配置(已添加 /book/reader)
|
||||||
|
|
||||||
|
已修改的文件:
|
||||||
|
- `src/views/crud_book/CrudBookModelViewSet/crud.tsx` - 添加"在线阅读"按钮
|
||||||
|
|
||||||
|
### 3. 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
# 或
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 上传测试文件
|
||||||
|
|
||||||
|
1. 准备一个EPUB格式的电子书文件
|
||||||
|
2. 在图书管理页面,编辑或新增图书
|
||||||
|
3. 上传EPUB文件到"电子书文件"字段
|
||||||
|
4. 保存图书信息
|
||||||
|
|
||||||
|
### 2. 测试在线阅读
|
||||||
|
|
||||||
|
1. 在图书列表中找到已上传文件的图书
|
||||||
|
2. 点击"在线阅读"按钮
|
||||||
|
3. 验证阅读器是否正常加载
|
||||||
|
4. 测试功能:
|
||||||
|
- 翻页(上一页/下一页)
|
||||||
|
- 目录导航
|
||||||
|
- 字体大小调整
|
||||||
|
- 阅读进度保存
|
||||||
|
|
||||||
|
### 3. 测试阅读进度
|
||||||
|
|
||||||
|
1. 阅读几页后关闭阅读器
|
||||||
|
2. 重新打开同一本书
|
||||||
|
3. 验证是否自动跳转到上次阅读位置
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 阅读器无法加载图书?
|
||||||
|
|
||||||
|
**检查项:**
|
||||||
|
1. 确认EPUB文件已正确上传
|
||||||
|
2. 检查后端MEDIA配置是否正确
|
||||||
|
3. 查看浏览器控制台错误信息
|
||||||
|
4. 确认API接口返回正确的文件URL
|
||||||
|
|
||||||
|
### Q: 阅读进度无法保存?
|
||||||
|
|
||||||
|
**检查项:**
|
||||||
|
1. 确认用户已登录
|
||||||
|
2. 检查数据库迁移是否执行成功
|
||||||
|
3. 查看后端日志是否有错误
|
||||||
|
4. 确认ReadingProgress模型已创建
|
||||||
|
|
||||||
|
### Q: 前端启动失败?
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. 删除 `node_modules` 和 `package-lock.json`
|
||||||
|
2. 使用 `yarn install` 重新安装依赖
|
||||||
|
3. 如果使用npm,添加 `--legacy-peer-deps` 参数
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
✅ **已实现:**
|
||||||
|
- EPUB格式在线阅读
|
||||||
|
- 目录导航
|
||||||
|
- 阅读进度记录和恢复
|
||||||
|
- 字体大小调整
|
||||||
|
- 翻页控制
|
||||||
|
- 进度条显示
|
||||||
|
|
||||||
|
🚧 **待实现:**
|
||||||
|
- 翻译功能(需集成翻译API)
|
||||||
|
- 书签功能
|
||||||
|
- 笔记和高亮
|
||||||
|
- 夜间模式
|
||||||
|
- PDF格式支持
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### 2025-10-22
|
||||||
|
- ✅ 添加图书文件字段(file, file_type)
|
||||||
|
- ✅ 创建阅读进度模型(ReadingProgress)
|
||||||
|
- ✅ 实现文件获取API
|
||||||
|
- ✅ 实现阅读进度保存/获取API
|
||||||
|
- ✅ 前端添加阅读器路由
|
||||||
|
- ✅ 图书列表添加"在线阅读"按钮
|
||||||
|
- ✅ 完成前后端集成
|
||||||