Python Django restframework 美多商城项目(一)——项目准备

创建一个新DRF项目的步骤

创建项目目录:

在命令行中创建一个新的目录用于存放你的项目文件:

   mkdir my_drf_project
   cd my_drf_project

创建虚拟环境:

在项目目录内创建一个Python虚拟环境(一个项目一个虚拟环境,防止互相影响):

   python -m venv venv

激活虚拟环境:

在Windows上是

venv\Scripts\activate

在Unix或MacOS系统上是

source venv/bin/activate

安装Django和DRF:

在激活的虚拟环境中,安装Django和DRF:

   pip install django djangorestframework

创建新的Django项目:

使用Django命令行工具创建新项目:

   django-admin startproject projectname

其中 projectname 是你的项目名。

创建新的Django应用:

在Django项目内创建一个或多个应用:

   python manage.py startapp appname

其中 appname 是你的应用名。

指定应用存放路径:

   python manage.py startapp users my_project/apps/users

my_project/apps/user 这个路径必须已经存在

加入DRF到Django settings:

在项目的 settings.py 文件中加入 rest_frameworkINSTALLED_APPS:

   INSTALLED_APPS = [
       # ...
       'rest_framework',
   ]

配置你的DRF应用:

根据你的需要配置DRF。例如,设置分页,认证,权限等:

   REST_FRAMEWORK = {
       'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
       'PAGE_SIZE': 10,
       # Other configurations...
   }

创建你的模型和视图:

在你的Django应用中创建模型(models.py)和序列化类(通常在 serializers.py 中)。然后,在 views.py 里创建视图集(ViewSets)或视图(Views)。

配置URLs:

使用DRF的路由器(router)来自动生成URL配置,这通常会在 urls.py 文件中做:

   from django.urls import include, path
   from rest_framework import routers
   from appname import views

   router = routers.DefaultRouter()
   router.register(r'yourmodel', views.YourModelViewSet)

   urlpatterns = [
       path('', include(router.urls)),
   ]

设定数据库和迁移操作:

配置 settings.py 里的数据库设置,并执行迁移来为你的模型创建数据库表:

    python manage.py makemigrations
    python manage.py migrate

运行开发服务器:

启动Django项目:

    python manage.py runserver

mysql数据库配置及驱动配置

创建数据库

  • 查看已有数据库命令

    SHOW DATABASES;
    
  • 创建新数据库

    CREATE DATABASE 数据库名 CHARACTER SET utf8mb4;
    

    utf8mb4 和 utf8 的区别主要是 utf8mb4可以存储4字符的内容,而utf8只能存储3及3一下字符,一般使用utf8mb4即可,因为实际使用对若不存储4字符的内容两种格式的存储空间一样。

  • 创建一个用户管理此数据库

    • 创建用户:
    CREATE USER '用户名' IDENTIFIED BY '密码';
    
    • 添加权限:给予用户数据库中所有表的所有权限
    GRANT ALL ON 数据库名.* TO '用户名'@'%';
    
    • 刷新权限设置
    FLUSH PRIVILEGES;
    
  • 创建一个用户只能从指定主机连接数据库

    • 创建用户:
    CREATE USER '用户名'@'host' IDENTIFIED BY '密码';
    

    host 可以是具体的IP地址、主机名、一个子网或localhost。

    • 指定权限
    GRANT ALL ON 数据库名.* TO '用户名'@'host';
    
    • 刷新权限
    FLUSH PRIVILEGES;
    
  • 给一个用户仅有部分管理权限

    GRANT 权限类型1, 权限类型2 ON 数据库名.表名 TO '用户名'@'host';
    FLUSH PRIVILEGES;
    

    多个权限类型用户逗号隔开,权限类型包括:

    • 查询(SELECT)

    • 插入(INSERT)

    • 更新(UPDATE)

    • 删除(DELETE)

    • 执行(EXECUTE)

    • 显示视图(SHOW VIEW)

    • 创建(CREATE)

    • 修改(ALTER)

    • 索引管理(INDEX)

    • 删除(DROP)

    • 创建临时表(CREATE TEMPORARY TABLES)

    • 锁表(LOCK TABLES)

    • 创建视图(CREATE VIEW)

    • 事件(EVENT)

    • 触发器(TRIGGER)
      等等权限。

数据库连接配置

setting.py 中修改DATABASES:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',  # 数据库主机
        'PORT': 3306,  # 数据库端口
        'USER': 'meiduo',  # 数据库用户名
        'PASSWORD': 'meiduo',  # 数据库用户密码
        'NAME': 'meiduo_mall'  # 数据库名字
    }
}

安装mysql客户端库,这里使用mysqlclient,它包括C语音扩展,相较于纯python编写的pymysql性能更高。

pip install mysqlclient

redis数据库配置

安装redis

pip install django-redis

会自动安装python redis客户端

配置redis

settings.py

# redis
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/0',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    },
    'session': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}


# 指定会话引擎
SESSION_ENGINE = "django.contrib.sessions.backends.cache"

# 指定会话缓存别名
SESSION_CACHE_ALIAS = "session"

# 根据您之前提供的配置,还可以设置会话的其他属性,例如:
# SESSION_COOKIE_AGE 用来设置cookie的过期时间,默认是两周。
# SESSION_SAVE_EVERY_REQUEST 如果设置为True,每次请求都会保存session信息,默认是False
# SESSION_EXPIRE_AT_BROWSER_CLOSE 如果设置为True,当浏览器关闭时session会结束,默认是False。

集成日志输出器

settings.py

LOGGING = {
    'version': 1,  # 日志配置的版本,目前必须是1。
    'disable_existing_loggers': False,  # 是否禁用Django默认创建的日志器。
    'formatters': {  # 定义日志信息的输出格式。
        'verbose': {  # 一个名为verbose的格式器。
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',  # 输出格式。
            'style': '{',  # 使用新式的字符串格式化。
        },
        'simple': {  # 一个名为simple的格式器。
            'format': '{levelname} {message}',  # 输出格式。
            'style': '{',  # 使用新式的字符串格式化。
        },
    },
    'filters': {  # 对日志进行过滤
        'require_debug_true': {  # django在debug模式下才输出日志
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {  # 定义了如何处理日志记录。
        'file': {  # 一个名为file的处理器,用于将日志记录到文件。
            'level': 'DEBUG',  # 处理器处理的最低日志级别。
            'class': 'logging.FileHandler',  # 使用文件处理器类。
            'filename': '/path/to/your/logs/django.log',  # 日志文件的位置。
            'formatter': 'verbose',  # 使用上面定义的verbose格式器。
        },
        'console': {  # 一个名为console的处理器,用于输出到控制台。
            'level': 'DEBUG',  # 处理器处理的最低日志级别。
            'class': 'logging.StreamHandler',  # 使用流处理器类。
            'formatter': 'simple',  # 使用上面定义的simple格式器。
            'filters': ['require_debug_true'],  # 用过滤器使只有在debug模式下才在控制台输出日志
        },
    },
    'loggers': {  # 定义日志记录器,指定日志的来源。
        'django': {  # Django自带的日志记录器。
            'handlers': ['file', 'console'],  # 指定该记录器使用的处理器。
            'level': 'DEBUG',  # 记录器记录的最低日志级别。
            'propagate': True,  # 是否将消息传递给更高级别(具有父子关系的)的记录器。
        },
        'django.request': {  # Django用于捕获请求异常的记录器。
            'handlers': ['file'],  # 指定处理器。
            'level': 'ERROR',  # 记录级别。
            'propagate': False,  # 此记录器的日志消息不会被传递给更高级别的记录器。
        },
        # 为你的 DRF 应用程序添加自定义记录器的示例:
        'myapp': {
            'handlers': ['file', 'console'],  # 同时输出到文件和控制台。
            'level': 'DEBUG',  # 记录的最低等级。
        },
    }
}

每个键的作用如下:

  • version: 日志配置字典的版本,当前只支持版本1。
  • disable_existing_loggers: 如果设置为True,那么在配置LOGGING之前已经创建的日志器将会被禁用。
  • formatters: 定义输出格式的字典。可以为每个记录器或处理器指定一个格式器。
  • filters: 定义过滤器,可选。允许在日志记录发生前对其进行额外的处理,比如根据特定的条件判断是否要输出该条日志记录。
  • handlers: 定义怎样处理(例如,输出、保存)日志记录的字典。每个处理器可以指定一个输出级别和一个格式器。
  • loggers: 定义日志记录器字典,网制定哪些日志需要被记录,以及如何处理它们。

在配置日志时,可以选择不同级别的日志输出,常见等级(由低到高)如下:

  • DEBUG: 详尽信息,通常只在诊断问题时产生。
  • INFO: 证明事情按预期工作。
  • WARNING: 表明发生了不期望的事或问题很快就会发生(例如‘磁盘空间低’)。该软件仍按预期工作。
  • ERROR: 由于更严重的问题,软件无法执行某些功能。
  • CRITICAL: 严重错误,表明程序本身可能无法继续运行。

自定义异常捕获

drf框架自带的异常捕获不包括数据库和redis,所以我们要通过自定义的方式加入mysql和redis的异常捕获。

在项目目录下创建utils文件夹,添加exceptions.py文件

exceptions.py :

import logging
from django.db import DatabaseError
from redis.exceptions import RedisError
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler

# 获取在配置文件中定义的logger, 用来记录日志
logger = logging.getLogger('django')


def exception_handler(exc, context):
    """
    自定义异常处理方法
    :param exc: 异常实例对象
    :param context: 抛出异常的上下文(包含view、request等)
    :return: Response对象
    """

    # 调用drf框架原生的异常处理方法
    response = drf_exception_handler(exc, context)

    if response is not None:
        view = context['view']
        if isinstance(exc, DatabaseError):
            # 数据库异常
            logger.error(f"Database error in {view}: {exc}")
            response = Response({'error': '数据库异常'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
        elif isinstance(exc, RedisError):
            # Redis异常
            logger.error(f"Redis error in {view}: {exc}")
            response = Response({'error': 'Redis异常'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)

    return response

settings.py:

REST_FRAMEWORK = {
    # 异常处理
    # 'EXCEPTION_HANDLER': 'your_project.your_app.module.exception_handler',
    'EXCEPTION_HANDLER': 'meiduo_mall.utils.exceptions.exception_handler',
}

解决前后端跨域

跨域产生的原因-同源策略

同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

同源是指:协议相同、域名/IP相同、端口相同。

浏览器只阻止表单以及 ajax 请求,并不会阻止 src 请求,所以能访问CDN,图片等 src 请求。

使用CORS解决跨域问题

CORS,Cross-Origin Resource Sharing跨来源资源共享,是一个新的 W3C 标准,它新增的一组HTTP首部字段,允许服务端其声明哪些源站有权限访问哪些资源。换言之,它允许浏览器向声明了 CORS 的跨域服务器,发出 XMLHttpReuest 请求,从而克服 Ajax 只能同源使用的限制。在我们的django框架中就是利用CORS来解决跨域请求的问题。

安装配置django-cors-headers

安装

pip install django-cors-headers

settings.py 内进行配置

INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'corsheaders',  # 跨域
    ...
]

MIDDLEWARE:

放到第一行,因为所有请求进行来都要先允许白名单访问,否则后面的程序都执行不到。

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 跨域
    ...
]

允许跨域请求追加白名单

新版本中使用CORS_ALLOWED_ORIGINS,老版本使用CORS_ORIGIN_WHITELIST ,目前两个都可以,建议选新的。

必须加http://或https://,否则会报错。

CORS_ALLOWED_ORIGINS = (
    'http://127.0.0.1:8080',
    'http://localhost:8080',
)
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']

celery安装配置

Celery 是一个强大的分布式任务队列系统,它用于异步执行后台任务。当你有一些耗时的处理工作,比如发送电子邮件、生成报告、同步数据等,而你不想用户等待这些操作完成时,就可以使用 Celery 将这些任务放到后台异步处理。

下面是一些常见的 Celery 使用场景:

  1. 后台任务:长时间运行的任务,比如批量处理或计算密集型任务,可以在后台异步执行,提高前端响应时间。
  2. 定时任务:通过Celery的周期性任务调度,你可以设定定时任务,类似于Linux的cronjobs。
  3. 并行处理:可以在多个进程、线程或者机器中并行处理任务,提高效率。
  4. 微服务架构:在微服务架构中,可以用Celery在不同的服务间异步传递消息。

安装配置

安装celery

pip install celery

配置celery

在项目目录下创建celery_tasks python包

celery_tasks内创建config.py文件

# celery配置文件

# 指定任务队列的位置
broker_url = 'redis://127.0.0.1:6379/7'

# 启动时重试连接中间件的话
broker_connection_retry = True
broker_connection_retry_on_startup = True

celery_tasks内创建main.py文件

# celery启动文件

from celery import Celery

# 1. 创建 Celery 实例
celery_app = Celery('meiduo')

# 2.加载配置文件
celery_app.config_from_object('celery_tasks.config')

# 3.自动注册异步任务
celery_app.autodiscover_tasks(['celery_tasks.sms'])  # celery_tasks.sms为自定义的任务

celery_tasks创建python包sms,这是一个发短信的任务,目录内创建tasks.py文件定义任务。

注意定义任务的文件名必须为tasks.py。

示例代码:

# 编辑异步任务代码
from celery_tasks.main import celery_app


@celery_app.task(name='sms_task')
def multiply(x, y):
    return x * y

调用

#导包
from celery_tasks.sms.tasks import multiply

multiply.delay(4, 4)  # 加delay()触发异步任务,multiply(4, 4)调用普通函数而已

启动任务

celery -A celery_tasks.main worker -l info  # celery_tasks.main为要加载的模块

选择执行池

在Celery中,--pool参数用于指定worker的执行池(execution pool),这决定了Celery将以何种方式处理并发任务。不同的pool类型会用不同的方式来处理并发执行任务,而这直接影响着性能和适用性。以下是一些常见的pool类型及其说明:

  1. prefork(默认)
    • Prefork pool使用预先分叉(fork)的工作进程来执行任务。
    • 它适用于CPU密集型的任务和需要隔离的环境。
    • 它是默认选项,不需要额外的配置即可使用。
    • 在类Unix系统上表现良好,但不建议在Windows系统上使用,因为Windows不支持prefork模式的全功能。
  2. solo
    • Solo pool在主进程中直接运行任务,不创建任何子进程或线程。
    • 这种方式适用于调试和开发环境,因为它能确保异常和调试信息都在同一个进程中输出,但不适合生产环境。
    • 它通常不支持并发执行任务,任务会被串行处理。
  3. eventlet
    • Eventlet是一个基于协程的并发库,让你可以以非常轻量级的形式实现并发。
    • Celery的eventlet pool通过协程来实现并发处理任务。
    • 适用于I/O密集型任务,因为它利用协程在I/O等待时切换任务。
    • 当使用eventlet时,任务能够以并发的方式执行,但它们实际上是在同一个进程中异步运行的。
  4. gevent
    • Gevent也是一个基于协程的并发库,类似于eventlet。
    • 它实现了自己的高效事件循环,并且能够进行快速的上下文切换。
    • gevent同样适合I/O密集型任务,并且在处理大量并发连接时性能出色。
  5. threads
    • 线程池利用多线程来处理并发任务。
    • 对于I/O密集型的任务,线程池是一个不错的选择,特别是当与操作系统或某些库的线程支持相结合时。

在选择pool时,你需要根据你的任务类型、系统环境以及Celery版本来确定最合适的pool类型。例如,在Windows系统上由于不支持prefork,推荐使用geventeventlet。对于生产环境,如果处理CPU密集型的任务,通常使用prefork是更好的选择。如果是I/O密集型任务,可能会倾向于使用eventletgevent

在命令行中指定--pool参数来选择不同类型的pool,例如:

celery -A your_project worker -l info --pool=gevent

这个命令将启动一个使用gevent为pool的Celery worker。如果你不指定--pool,Celery会使用默认的prefork

CPU密集型(CPU-bound)和I/O密集型(I/O-bound)是描述计算机程序运行特性的两个术语。它们主要反映了程序运行时资源消耗的差异,下面详细解释这两者的区别,并给出具体的例子:

CPU密集型

CPU密集型程序或任务是指需要进行大量计算,消耗大量CPU资源,且CPU计算能力通常是性能瓶颈的程序或任务。这类程序执行时CPU的利用率非常高,因为几乎所有的时间都在进行数学计算、逻辑判断等内存或CPU周期密集型的操作。

举例

  • 数字加密和解密:涉及大量的数学运算,尤其是在处理大键或复杂算法时。
  • 视频编解码:转换视频格式或质量时,CPU需要对大量数据执行复杂的转换算法。
  • 大数据分析:在分析大量数据时,比如进行数据挖掘或运行复杂的统计模型时。
  • 科学计算:进行物理模拟、天气预测或生物信息学计算等。

I/O密集型(I/O-bound)

I/O密集型指的是程序或任务的瓶颈主要在于等待I/O操作完成,比如磁盘操作、网络请求等。这类程序执行时CPU利用率不会很高,因为CPU大部分时间都在等待数据的读写操作完成。实际上,I/O密集型程序的执行性能通常受限于系统的I/O能力,例如硬盘读写速度、网络带宽等。

举例

  • 文件处理:对磁盘上的文件进行读写操作,如日志处理、数据备份。
  • 数据库操作:频繁的查询和更新数据库时,需要等待磁盘I/O和网络I/O。
  • 网络应用:如Web服务器处理HTTP请求,通常需要在网络连接上进行大量数据的发送和接收。
  • 客户服务系统:可能包括大量的客户端请求,涉及到对数据库的读取以及网络响应的等待。

在设计和优化系统时,我们需要根据所处理的是CPU密集型任务还是I/O密集型任务,选择合适的架构和资源配置。例如,对于CPU密集型任务,我们可能需要更强的CPU能力和适合的并行计算策略;而对于I/O密集型任务,则可能需要更快的存储设备和高效的I/O处理机制。

使用celery gevent 发送短信和邮件

步骤1: 安装所需的Python包

首先,确保你的环境中安装了celerygevent。如果还未安装,可以通过pip进行安装:

pip install celery gevent

步骤2: 创建Celery实例

在你的项目中创建一个新的Python文件,例如tasks.py,并在其中创建一个Celery实例:

from celery import Celery

app = Celery('my_tasks', 
             broker='你的消息代理地址,如: redis://localhost:6379/0',
             backend='你的结果存储地址,如: redis://localhost:6379/0')

@app.task
def send_email(email_address, message):
    # 这里模拟发送邮件的操作
    print(f"发送邮件到 {email_address}: {message}")
    return f"邮件已发送到 {email_address}"

步骤3: 配置Celery以使用gevent

为了让Celery使用gevent进行并发,你需要在启动worker时指定-P gevent参数。此外,为了提高并发性能,你可以通过-c参数指定并发worker的数量。这里的数量取决于你的具体任务和资源,可以进行适当的调整。

celery -A tasks worker -l info -P gevent -c 1000

这个命令会启动Celery worker,并将gevent作为并发池,设置并发worker数量为1000。你可以根据实际需求调整这个数量。

步骤4: 调用异步任务

现在,你可以在任何需要的地方调用定义好的异步任务了。以下是一个调用send_email任务的例子:

from tasks import send_email

send_email.delay("example@example.com", "Hello, this is a test email.")

使用delay方法会异步地发送任务到消息队列中,Celery的worker会捕获这个任务并使用gevent来并发执行。

注意事项

  • 确保消息代理和结果存储(如Redis或RabbitMQ)已经正确设置并可以访问。
  • 在高并发场景下,gevent的性能通常比较好,但是需要注意,IO操作(如访问网络或数据库)应该是非阻塞的;如果有阻塞调用,需要使用对应的gevent兼容库,否则并发性能可能会受到影响。
  • 测试和调整并发worker的数量以适应你的具体应用场景。太高的并发数可能会导致资源竞争,太低则无法充分利用资源。

通过以上步骤,你就可以利用Celery和gevent实现高效的异步任务处理了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350