Compare commits

..

No commits in common. "f3ede6cb199342a660a552a8a4c5b978cfcddd65" and "345da7ed48e2458b8b1f344bb8952d4f9cff5f18" have entirely different histories.

89 changed files with 25 additions and 335407 deletions

4
.gitignore vendored
View File

@ -1,4 +0,0 @@
application/__pycache__/__init__.cpython-311.pyc
logs/*.log
*.pyc
__pycache__/

View File

@ -1,52 +0,0 @@
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', 'redis') # redis
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
def is_tenants_mode():

View File

@ -70,12 +70,12 @@ My_Apps = [
INSTALLED_APPS += My_Apps
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # 跨域中间件
"django.middleware.common.CommonMiddleware",
"dvadmin.utils.middleware.HealthCheckMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware", # 跨域中间件
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
@ -162,7 +162,6 @@ STATICFILES_DIRS = [
MEDIA_ROOT = "media" # 项目下的目录
MEDIA_URL = "/media/" # 跟STATIC_URL类似指定用户可以通过这个url找到文件
#添加以下代码以后就不用写{% load staticfiles %},可以直接引用
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
@ -172,41 +171,12 @@ 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, # 读写超时时间(秒)
}
}
}
# ================================================= #
# ******************* 跨域的配置 ******************* #
# ================================================= #
# 全部允许配置
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
"http://127.0.0.1:8080",
]
# 允许cookie
CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中后端是否支持对cookie的操作
@ -214,21 +184,19 @@ 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": [(f'{REDIS_HOST}', 6379)],
"password": REDIS_PASSWORD if REDIS_PASSWORD else None,
"db": REDIS_DB,
},
},
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# CHANNEL_LAYERS = {
# 'default': {
# 'BACKEND': 'channels_redis.core.RedisChannelLayer',
# 'CONFIG': {
# "hosts": [('127.0.0.1', 6379)], #需修改
# },
# },
# }
# ================================================= #
@ -418,19 +386,6 @@ 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"
@ -468,6 +423,3 @@ from dvadmin3_celery.settings import * # celery 异步任务
#from dvadmin_uniapp.settings import *
# ...
# ********** 一键导入插件配置结束 **********
# api地址
API_URL = 'http://127.0.0.1:8000/'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,29 +0,0 @@
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,13 +30,9 @@ TABLE_PREFIX = "dvadmin_"
# ================================================= #
REDIS_DB = 1
CELERY_BROKER_DB = 3
REDIS_PASSWORD = '' # 无密码的Redis服务器
REDIS_PASSWORD = 'DVADMIN3'
REDIS_HOST = '127.0.0.1'
# 根据是否有密码生成正确的Redis URL
if REDIS_PASSWORD:
REDIS_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:6379'
else:
REDIS_URL = f'redis://{REDIS_HOST}:6379'
REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6379'
# ================================================= #
# ****************** 功能 启停 ******************* #
# ================================================= #

View File

@ -1,58 +0,0 @@
# 数据库迁移说明
## 新增字段和模型
本次更新为图书管理系统添加了以下功能:
### 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. 阅读进度按用户和图书唯一,同一用户同一本书只有一条进度记录

View File

@ -1,18 +0,0 @@
# 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='封面'),
),
]

View File

@ -1,26 +0,0 @@
# 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='文件类型'),
),
]

View File

@ -1,23 +0,0 @@
# 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='封面'),
),
]

View File

@ -1,18 +0,0 @@
# 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='封面'),
),
]

View File

@ -1,18 +0,0 @@
# 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='封面'),
),
]

View File

@ -1,18 +0,0 @@
# 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='副标题'),
),
]

View File

@ -1,37 +0,0 @@
# 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

@ -1,14 +0,0 @@
# 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

@ -1,61 +0,0 @@
# 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,5 +1,4 @@
from django.db import models
from django.conf import settings
# Create your models here.
from dvadmin.utils.models import CoreModel
@ -7,7 +6,7 @@ from dvadmin.utils.models import CoreModel
class CrudBookModel(CoreModel):
title = models.CharField(max_length=255, verbose_name="书名")
sub_title = models.CharField(max_length=255, verbose_name="副标题", null=True, blank=True)
sub_title = models.CharField(max_length=255, verbose_name="副标题")
series = models.CharField(max_length=255, verbose_name="系列丛书", null=True, blank=True)
author = models.CharField(max_length=255, verbose_name="作者")
translator = models.CharField(max_length=255, verbose_name="译者", null=True, blank=True)
@ -21,48 +20,11 @@ class CrudBookModel(CoreModel):
# create_by = models.IntegerField(verbose_name="创建者编号", default=1)
# update_time = models.DateField(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.CharField(max_length=255,verbose_name="封面", null=True, blank=True)
image = models.ImageField(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',)
ordering = ('-create_datetime',)

View File

@ -1,44 +1,13 @@
#backend/crud_demo/serializers.py
from crud_book.models import CrudBookModel, ReadingProgress
from crud_book.models import CrudBookModel
from dvadmin.utils.serializers import CustomModelSerializer
from rest_framework import serializers
from django.conf import settings
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:
model = CrudBookModel
@ -52,49 +21,4 @@ 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)
fields = '__all__'

View File

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

View File

@ -1,15 +1,6 @@
# Create your views here.
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 crud_book.models import CrudBookModel
from crud_book.serializers import CrudBookModelSerializer, CrudBookModelCreateUpdateSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@ -24,153 +15,4 @@ class CrudBookModelViewSet(CustomModelViewSet):
queryset = CrudBookModel.objects.all()
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
)
update_serializer_class = CrudBookModelCreateUpdateSerializer

Binary file not shown.

View File

@ -1,91 +0,0 @@
"""
修复数据库中文件路径的反斜杠问题
将所有 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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -9,7 +9,6 @@ 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

@ -1,171 +0,0 @@
# 在线阅读功能部署说明
## 后端部署步骤
### 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
- ✅ 前端添加阅读器路由
- ✅ 图书列表添加"在线阅读"按钮
- ✅ 完成前后端集成