在线阅读 Redis支持服务无密码

This commit is contained in:
liurui 2025-10-23 01:10:28 +08:00
parent 02f2e1d972
commit b0931ae38c
34 changed files with 333169 additions and 44 deletions

52
add_book_file.py Normal file
View 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的图书')

View File

@ -5,7 +5,7 @@ from django.db import connection
from django.core.cache import cache
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():

View File

@ -172,6 +172,31 @@ STATICFILES_FINDERS = (
# python manage.py collectstatic
# 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, # 读写超时时间(秒)
}
}
}
# ================================================= #
# ******************* 跨域的配置 ******************* #
# ================================================= #
@ -189,19 +214,21 @@ CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持
# ********************* channels配置 ******************* #
# ===================================================== #
ASGI_APPLICATION = 'application.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
# 'CONFIG': {
# "hosts": [('127.0.0.1', 6379)], #需修改
# },
# },
# "default": {
# "BACKEND": "channels.layers.InMemoryChannelLayer"
# }
# }
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,
},
},
}
# ================================================= #
@ -391,6 +418,19 @@ API_MODEL_MAP = {
DJANGO_CELERY_BEAT_TZ_AWARE = False
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"

29
check_book_file.py Normal file
View 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}')

Binary file not shown.

View File

@ -30,9 +30,13 @@ TABLE_PREFIX = "dvadmin_"
# ================================================= #
REDIS_DB = 1
CELERY_BROKER_DB = 3
REDIS_PASSWORD = 'DVADMIN3'
REDIS_PASSWORD = '' # 无密码的Redis服务器
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'
# ================================================= #
# ****************** 功能 启停 ******************* #
# ================================================= #

View File

@ -23,27 +23,4 @@ class Migration(migrations.Migration):
name='file_type',
field=models.CharField(blank=True, help_text='epub, pdf, mobi等', max_length=50, null=True, verbose_name='文件类型'),
),
migrations.CreateModel(
name='ReadingProgress',
fields=[
('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')),
('description', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')),
('modifier', models.CharField(blank=True, help_text='修改人', max_length=255, null=True, verbose_name='修改人')),
('dept_belong_id', models.CharField(blank=True, help_text='数据归属部门', max_length=255, 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(help_text='EPUB CFI 或其他位置标识', verbose_name='阅读位置')),
('progress', models.FloatField(default=0, help_text='百分比0-100', verbose_name='阅读进度')),
('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reading_progresses', to='crud_book.crudbookmodel', verbose_name='图书')),
('creator', models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reading_progresses', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '阅读进度',
'verbose_name_plural': '阅读进度',
'db_table': 'reading_progress',
'ordering': ('-update_datetime',),
'unique_together': {('user', 'book')},
},
),
]

View 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')},
},
),
]

View 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 = [
]

View File

@ -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='用户'),
),
]

View File

@ -1,4 +1,5 @@
from django.db import models
from django.conf import settings
# Create your models here.
from dvadmin.utils.models import CoreModel
@ -23,9 +24,45 @@ class CrudBookModel(CoreModel):
# 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)
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:
db_table = "books"
verbose_name = '图书表'
verbose_name_plural = verbose_name
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',)

View File

@ -1,6 +1,6 @@
#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 rest_framework import serializers
from django.conf import settings
@ -53,3 +53,48 @@ class CrudBookModelCreateUpdateSerializer(CustomModelSerializer):
class Meta:
model = CrudBookModel
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)

View File

@ -2,11 +2,12 @@
from rest_framework.routers import SimpleRouter
from .views import CrudBookModelViewSet
from .views import CrudBookModelViewSet, ReadingProgressViewSet
router = SimpleRouter()
# 这里进行注册路径并把视图关联上这里的api地址以视图名称为后缀这样方便记忆api/CrudBookModelViewSet
router.register("api/CrudBookModelViewSet", CrudBookModelViewSet)
router.register("api/reading-progress", ReadingProgressViewSet)
urlpatterns = [
]

View File

@ -1,6 +1,15 @@
# Create your views here.
from crud_book.models import CrudBookModel
from crud_book.serializers import CrudBookModelSerializer, CrudBookModelCreateUpdateSerializer
import os
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
@ -16,3 +25,152 @@ class CrudBookModelViewSet(CustomModelViewSet):
serializer_class = CrudBookModelSerializer
create_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
)

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
media/books/hamlet.epub Normal file

Binary file not shown.

Binary file not shown.

View File

@ -9,6 +9,7 @@ django-simple-captcha==0.6.0
django-timezone-field==7.0
djangorestframework_simplejwt==5.4.0
drf-yasg==1.21.7
django-redis==5.4.0
pypinyin==0.51.0
ua-parser==0.18.0

View 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
- ✅ 前端添加阅读器路由
- ✅ 图书列表添加"在线阅读"按钮
- ✅ 完成前后端集成