基于Laravel自定义trait实现统一API接口返回规范

一、效果展示

最终实现效果如下图所示:
支持i18n国际化数据返回,修改不同'locale' => 'zh_CN',返回中文
'locale' => 'en',返回英文。
当前使用版本:PHP Version 8.2.9 、Laravel10x。

image.png

  • 有数据返回:
 public function token(Request $request)
   {
        $request->validate([
            'name' => 'required',
            'password' => 'required',
            'device_name' => 'required',
        ]);

        $user = User::where('name', $request->name)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['用户名或密码不正确!'],
            ]);
        }

        return $this->success(["token" => $user->createToken($request->device_name)->plainTextToken]);
}
{
    "code": 200,
    "message": "请求成功",
    "data": {
        "token": "49|SpUVNfkiPQhd8aY2BffAGNmN8e8ywMtHw9L0k2I97c7bf39a"
    }
}
 public function show(Request $request)
 {
        return $this->success(['user' => User::all()]);
 }
{
    "code": 200,
    "message": "请求成功",
    "data": {
        "user": [
            {
                "id": 1,
                "name": "admin",
                "email": "admin@qq.com",
                "email_verified_at": null,
                "created_at": null,
                "updated_at": null
            }
        ]
    }
}
  • 无需数据返回
  public function index()
  {
        return $this->ok();
  }
{
    "code": 200,
    "message": "请求成功"
}

二、实现参考

在app目录下,新建\Helpers\Api子目录


image.png

代码实现如下,

<?php

namespace App\Helpers\Api;

use Illuminate\Http\Response as FoundationResponse;

trait ApiResponse
{
    protected $statusCode = FoundationResponse::HTTP_OK;

    /**
     * @return mixed
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * @param $statusCode
     * @return $this
     */
    public function setStatusCode($statusCode)
    {
        $this->statusCode = $statusCode;
        return $this;
    }

    /**
     * 自定义发送数据
     * @param $data
     * @param array $header
     * @return mixed
     */
    public function respond($data, $header = [])
    {
        return Response()->json($data, $this->getStatusCode(), $header);
    }

    /**
     * 生成发送数据
     * @param string $message
     * @param array $data
     * @param $code
     * @return mixed
     */
    private function makeDataAndRespond(array $data, string $message, $code = 200)
    {
        $this->setStatusCode($code ?? 200);

        $arrData = [
            'code' => $this->statusCode,
            'message' => $message
        ];
        $ret = array_merge($arrData, ["data" => $data]);
        return $this->respond($ret);
    }

    /**
     * 仅发送消息
     * @param string $message
     * @param $code
     * @return mixed
     */
    public function message(string $message, $code = 200)
    {
        $this->setStatusCode($code ?? 200);

        $arrData = [
            'code' => $this->statusCode,
            'message' => $message
        ];
        return $this->respond($arrData);
    }

    /**
     * 返回成功
     * http_status始终为200
     * code可以修改
     * 不带data参数
     * @param string|NULL $message
     * @param $code
     * @return mixed
     */
    public function ok(string $message = NULL, $code = 200)
    {
        $this->setStatusCode(200);

        $arrData = [
            'code' => $code ?? 200,
            'message' => $message ?? __("http-statuses.200")
        ];
        return $this->respond($arrData);
    }

  /**
     * 发送数据
     * @param $data
     * @param string|NULL $message
     * @return mixed
     */
    public function success($data = null, string $message = NULL)
    {
        $code = FoundationResponse::HTTP_OK;
        if (!is_array($data)) {
            return $this->respond(['code' => $code,
                    'message' => $message ?? __("http-statuses." . $code),
                    'data' => $data]
            );
        }
        return $this->makeDataAndRespond($data, $message ?? __("http-statuses." . $code));
    }

    /**
     * 发送失败消息
     * @param $message
     * @param $code
     * @return mixed
     */
    public function failed(string $message = NULL, $code = FoundationResponse::HTTP_BAD_REQUEST)
    {
        return $this->message($message ?? __("http-statuses." . $code), $code);
    }

    /**
     * 已创建
     * @param string|NULL $message
     * @return mixed
     */
    public function created(string $message = NULL)
    {
        return $this->failed($message, FoundationResponse::HTTP_CREATED);
    }

    public function locked(string $message = NULL)
    {
        return $this->failed($message, FoundationResponse::HTTP_LOCKED);
    }

    /**
     * 内部错误
     * @param string|NULL $message
     * @return mixed
     */
    public function internalError(string $message = NULL)
    {
        return $this->failed($message, FoundationResponse::HTTP_INTERNAL_SERVER_ERROR);
    }

    /**
     * 未找到
     * @param string|null $message
     * @return mixed
     */
    public function notFound(string $message = null)
    {
        return $this->failed($message, Foundationresponse::HTTP_NOT_FOUND);
    }

    /**
     * 禁止访问
     * @param string|NULL $message
     * @return mixed
     */
    public function forbidden(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_FORBIDDEN);
    }

    /**
     * 无内容
     * @param string|NULL $message
     * @return mixed
     */
    public function noContent(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_NO_CONTENT);
    }

    /**
     * 未认证
     * @param string|NULL $message
     * @return mixed
     */
    public function unauthorized(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_UNAUTHORIZED);
    }

    /**
     * 网关错误
     * @param string|NULL $message
     * @return mixed
     */
    public function badGateway(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_BAD_GATEWAY);
    }

    /**
     * 未知错误
     * @param string|NULL $message
     * @return mixed
     */
    public function unknownError(string $message = NULL)
    {
        return $this->failed($message, 520);
    }

    /**
     * 版本不支持
     * @param string|NULL $message
     * @return mixed
     */
    public function versionNotSupported(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_VERSION_NOT_SUPPORTED);
    }

    /**
     * 连接超时
     * @param string|NULL $message
     * @return mixed
     */
    public function connectionTimedOut(string $message = NULL)
    {
        return $this->failed($message, 522);
    }

    /**
     * 已存在
     * @param string|NULL $message
     * @return mixed
     */
    public function found(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_FOUND);
    }

    /**
     * 已冲突
     * @param string|NULL $message
     * @return mixed
     */
    public function conflict(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_CONFLICT);
    }

    /**
     * 已不可用
     * @param string|NULL $message
     * @return mixed
     */
    public function gone(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_GONE);
    }

    /**
     * 已接受
     * @param string|NULL $message
     * @return mixed
     */
    public function accepted(string $message = NULL)
    {
        return $this->failed($message, Foundationresponse::HTTP_ACCEPTED);
    }
}

修改更符合axios.js等移动端请求,建议修改以下函数。实现,http_status始终返回200,而code进行变化。这样统一一下,移动端更容易统一处理。
即:请求所有接口,本身http状态始终为200,接口由code值变化而变化。

/**
     * 自定义发送数据
     * @param $data
     * @param array $header
     * @return mixed
     */
    public function respond($data, $header = [])
    {
        return Response()->json($data, $this->getStatusCode(), $header);
    }

修改为:

/**
     * 自定义发送数据
     * @param $data
     * @param array $header
     * @return mixed
     */
    public function respond($data, $header = [])
    {
        return Response()->json($data, FoundationResponse::HTTP_OK, $header);
    }

三、使用

在控制器类中,使用use ApiResponse;即可。

image.png

【需要安装laravel-lang】国际化支持。
image.png

<?php

namespace App\Http\Controllers;

use App\Helpers\Api\ApiResponse;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class UserController extends Controller
{
    use ApiResponse;

    public function index()
    {
        return $this->ok();
    }

    public function show(Request $request)
    {
        return $this->success(['user' => User::all()]);
    }

    public function token(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'password' => 'required',
            'device_name' => 'required',
        ]);

        $user = User::where('name', $request->name)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['用户名或密码不正确!'],
            ]);
        }

        return $this->success(["token" => $user->createToken($request->device_name)->plainTextToken]);
    }
}

备注【注意事项】:Postman接口设置X-Requested-With=XMLHttpRequest有利于调试,并且有异常都通过接口统一返回。Lavavel框架,依赖此字段判断是否为ajax的请求,否则会返回页面。Laravel整体使用非常Nice。

image.png

  • 这是uniapp中的配置参考
const BASE_URL = 'http://api.demo.test/api'

export const myRequest = (option) => {
    return new Promise ((resolve, reject) => {
        // 请求头信息
        const requestHeader = {
            // 获取手机C_ID 用于push动作
            // #ifdef APP-PLUS
            'C_ID': plus.push.getClientInfo().clientid || '',
            // #endif
            // 获取用户token
            'Authorization': 'Bearer ' + uni.getStorageSync('token'),
            // 做多点登录 PC APP 可同时登录两个账号
            'Os-Type': 'APP',
            "X-Requested-With":"XMLHttpRequest",
            'accept':'application/json',
            'content-type': 'application/x-www-form-urlencoded'
         }
            
        uni.request({
            url: BASE_URL + option.url,
            method: option.method || 'GET',
            data: option.data || {},
            header:requestHeader,
            dataType: 'json',
            success:(res) => {
                if(res.statusCode !== 200) {
                    console.log(res)
                    uni.showToast({
                        title:res.data.message
                    })
                    return
                }
                resolve(res.data)
            },fail: (err) => {
                uni.showToast({
                    title: '请求接口失败'
                })
                reject(err)
            }
        })
    })
}

  • 这是vue中,axios的配置参考
 // 发送 POST 请求
  axios({
    url: "http://api.demo.test/api/user/token",
    method: "POST",
    headers: { 
      "X-Requested-With":"XMLHttpRequest",
      'accept':'application/json',
      'content-type': 'application/x-www-form-urlencoded',
    },
    data: {
      name: "admin",
      password: "123456786",
      device_name:"WeX"
    },
  }).then((res) => {
    console.log(JSON.stringify(res));
  });
  • 错误码解释参考:
    http-statuses.php
<?php

declare(strict_types=1);

return [
    '0'            => '未知错误',
    '100'          => '继续请求',
    '101'          => '切换协议',
    '102'          => '处理中',
    '200'          => '请求成功',
    '201'          => '已创建',
    '202'          => '已接受',
    '203'          => '非权威信息',
    '204'          => '无内容',
    '205'          => '重置内容',
    '206'          => '部分内容',
    '207'          => '多状态',
    '208'          => '已上报',
    '226'          => 'IM已使用',
    '300'          => '多种选择',
    '301'          => '已永久移动',
    '302'          => '临时移动',
    '303'          => '见其他',
    '304'          => '未修改',
    '305'          => '使用代理',
    '307'          => '临时重定向',
    '308'          => '永久重定向',
    '400'          => '请求错误',
    '401'          => '未授权',
    '402'          => '需要付款',
    '403'          => '禁止',
    '404'          => '未找到',
    '405'          => '方法不允许',
    '406'          => '无法接受',
    '407'          => '需要代理验证',
    '408'          => '请求超时',
    '409'          => '冲突',
    '410'          => '不可用',
    '411'          => '长度要求',
    '412'          => '前提条件未满足',
    '413'          => '请求实体过大',
    '414'          => 'URI太长了',
    '415'          => '不支持的媒体类型',
    '416'          => '请求范围不符合',
    '417'          => '期望不满足',
    '418'          => '我是一个茶壶',
    '419'          => '会话已过期',
    '421'          => '错误的请求',
    '422'          => '不可处理的实体',
    '423'          => '锁定',
    '424'          => '失败的依赖',
    '425'          => '太早了',
    '426'          => '需要升级',
    '428'          => '前提要求',
    '429'          => '请求太多',
    '431'          => '请求标头字段太大',
    '444'          => '连接关闭无响应',
    '449'          => '重试',
    '451'          => '法律原因不可用',
    '499'          => '客户端关闭请求',
    '500'          => '内部服务器错误',
    '501'          => '未实现',
    '502'          => '网关错误',
    '503'          => '服务不可用',
    '504'          => '网关超时',
    '505'          => 'HTTP版本不支持',
    '506'          => '变体协商',
    '507'          => '存储空间不足',
    '508'          => '检测到环路',
    '509'          => '超出带宽限制',
    '510'          => '未延期',
    '511'          => '需要网络验证',
    '520'          => '未知错误',
    '521'          => 'Web服务器已关闭',
    '522'          => '连接超时',
    '523'          => '原点无法到达',
    '524'          => '发生超时',
    '525'          => 'SSL握手失败',
    '526'          => '无效的SSL证书',
    '527'          => '轨道炮错误',
    '598'          => '网络读取超时',
    '599'          => '网络连接超时',
    'unknownError' => '未知错误',
];

zh_CN.json

{
    "(and :count more error)": "(还有 :count 个错误)",
    "(and :count more errors)": "(以及另外 :count 个错误)",
    "A Timeout Occurred": "发生超时",
    "Accept": "接受",
    "Accepted": "已接受",
    "Action": "操作",
    "Actions": "操作",
    "Add": "添加",
    "Admin": "行政",
    "Agree": "同意",
    "All rights reserved.": "版权所有。",
    "Already Reported": "已上报",
    "Archive": "档案",
    "Assign": "分配",
    "Attach": "附加",
    "Bad Gateway": "网关错误",
    "Bad Request": "请求错误",
    "Bandwidth Limit Exceeded": "超出带宽限制",
    "Browse": "浏览",
    "Cancel": "取消",
    "Choose": "选择",
    "Choose :name": "选择:name",
    "Choose File": "选择文件",
    "Choose Image": "选择图片",
    "Click to copy": "点击复制",
    "Client Closed Request": "客户端关闭请求",
    "Close": "关闭",
    "Collapse": "坍塌",
    "Collapse All": "全部收缩",
    "Comment": "评论",
    "Confirm": "确认",
    "Conflict": "冲突",
    "Connect": "连接",
    "Connection Closed Without Response": "连接关闭无响应",
    "Connection Timed Out": "连接超时",
    "Continue": "继续请求",
    "Create": "创建",
    "Created": "已创建",
    "Delete": "删除",
    "Detach": "分离",
    "Details": "详情",
    "Disable": "禁用",
    "Discard": "丢弃",
    "Done": "完毕",
    "Down": "向下",
    "Duplicate": "复制",
    "Duplicate: name": "重复:名称",
    "Edit": "编辑",
    "Edit :name": "编辑:name",
    "Enable": "启用",
    "Expand": "扩张",
    "Expand All": "展开全部",
    "Expectation Failed": "期望不满足",
    "Explanation": "解释",
    "Export": "出口",
    "Failed Dependency": "失败的依赖",
    "File": "文件",
    "Files": "文件",
    "Forbidden": "访问被拒绝",
    "Found": "临时移动",
    "Gateway Timeout": "网关超时",
    "Go Home": "回首页",
    "Go to page :page": "前往第 :page 页",
    "Gone": "不可用",
    "Hello!": "您好!",
    "Hide": "隐藏",
    "Hide :name": "隐藏 :name",
    "Home": "家",
    "HTTP Version Not Supported": "HTTP版本不支持",
    "I'm a teapot": "我是一个茶壶",
    "If you did not create an account, no further action is required.": "如果您未注册帐号,请忽略此邮件。",
    "If you did not request a password reset, no further action is required.": "如果您未申请重设密码,请忽略此邮件。",
    "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "如果您单击「:actionText」按钮时遇到问题,请复制下方链接到浏览器中访问:",
    "IM Used": "IM已使用",
    "Image": "图像",
    "Impersonate": "模拟登录",
    "Impersonation": "冒充",
    "Import": "进口",
    "Import :name": "进口 :name",
    "Insufficient Storage": "存储空间不足",
    "Internal Server Error": "内部服务器错误",
    "Introduction": "介绍",
    "Invalid JSON was returned from the route.": "从路由返回无效的 JSON。",
    "Invalid SSL Certificate": "无效的SSL证书",
    "Length Required": "长度要求",
    "Like": "喜欢",
    "Load": "加载",
    "Localize": "本地化",
    "Locked": "锁定",
    "Login": "登录",
    "Logout": "登出",
    "Loop Detected": "检测到环路",
    "Maintenance Mode": "服务不可用",
    "Method Not Allowed": "方法不允许",
    "Misdirected Request": "错误的请求",
    "Moved Permanently": "已永久移动",
    "Multi-Status": "多状态",
    "Multiple Choices": "多种选择",
    "Network Authentication Required": "需要网络验证",
    "Network Connect Timeout Error": "网络连接超时",
    "Network Read Timeout Error": "网络读取超时",
    "New": "新建",
    "New :name": "新 :name",
    "No": "不",
    "No Content": "无内容",
    "Non-Authoritative Information": "非权威信息",
    "Not Acceptable": "无法接受",
    "Not Extended": "未延期",
    "Not Found": "页面不存在",
    "Not Implemented": "未实现",
    "Not Modified": "未修改",
    "of": "于",
    "OK": "请求成功",
    "Open": "打开",
    "Open in a current window": "在当前窗口中打开",
    "Open in a new window": "在新窗口中打开",
    "Open in a parent frame": "在父框架中打开",
    "Open in the topmost frame": "在最上面的框架中打开",
    "Open on the website": "在网站上打开",
    "Origin Is Unreachable": "原点无法到达",
    "Page Expired": "页面会话已超时",
    "Pagination Navigation": "分页导航",
    "Partial Content": "部分内容",
    "Payload Too Large": "请求实体过大",
    "Payment Required": "需要付款",
    "Permanent Redirect": "永久重定向",
    "Please click the button below to verify your email address.": "请点击下面按钮验证您的 E-mail:",
    "Precondition Failed": "前提条件未满足",
    "Precondition Required": "前提要求",
    "Preview": "预览",
    "Price": "价格",
    "Processing": "处理中",
    "Proxy Authentication Required": "需要代理验证",
    "Railgun Error": "轨道炮错误",
    "Range Not Satisfiable": "请求范围不符合",
    "Regards": "致敬",
    "Register": "注册",
    "Request Header Fields Too Large": "请求标头字段太大",
    "Request Timeout": "请求超时",
    "Reset Content": "重置内容",
    "Reset Password": "重置密码",
    "Reset Password Notification": "重置密码通知",
    "Restore": "恢复",
    "Restore :name": "恢复:name",
    "results": "结果",
    "Retry With": "重试",
    "Save": "保存",
    "Save & Close": "保存并关闭",
    "Save & Return": "保存并返回",
    "Save :name": "节省 :name",
    "Search": "搜索",
    "Search :name": "搜索 :name",
    "See Other": "见其他",
    "Select": "选择",
    "Select All": "全选",
    "Send": "发送",
    "Server Error": "服务器错误",
    "Service Unavailable": "服务不可用",
    "Session Has Expired": "会话已过期",
    "Settings": "设置",
    "Show": "展示",
    "Show :name": "显示 :name",
    "Show All": "显示所有",
    "Showing": "显示中",
    "Solve": "解决",
    "SSL Handshake Failed": "SSL握手失败",
    "Submit": "提交",
    "Subscribe": "订阅",
    "Switch": "转变",
    "Switch To Role": "切换角色",
    "Switching Protocols": "切换协议",
    "Tag": "标签",
    "Tags": "标签",
    "Temporary Redirect": "临时重定向",
    "The given data was invalid.": "给定的数据无效。",
    "The response is not a streamed response.": "该响应不是流式响应。",
    "The response is not a view.": "响应不是视图。",
    "This password reset link will expire in :count minutes.": "这个重设密码链接将会在 :count 分钟后失效。",
    "to": "至",
    "Toggle navigation": "切换导航",
    "Too Early": "太早了",
    "Too Many Requests": "请求次数过多。",
    "Translate": "翻译",
    "Translate It": "翻译它",
    "Unauthorized": "未授权",
    "Unavailable For Legal Reasons": "法律原因不可用",
    "Unknown Error": "未知错误",
    "Unpack": "打开包装",
    "Unprocessable Entity": "不可处理的实体",
    "Unsubscribe": "退订",
    "Unsupported Media Type": "不支持的媒体类型",
    "Up": "向上",
    "Update": "更新",
    "Update :name": "更新:name",
    "Upgrade Required": "需要升级",
    "URI Too Long": "URI太长了",
    "Use Proxy": "使用代理",
    "User": "用户",
    "Variant Also Negotiates": "变体协商",
    "Verify Email Address": "验证 E-mail",
    "View": "查看",
    "View :name": "查看 :name",
    "Web Server is Down": "Web服务器已关闭",
    "Whoops!": "哎呀!",
    "Yes": "是的",
    "You are receiving this email because we received a password reset request for your account.": "您收到此电子邮件是因为我们收到了您帐户的密码重设请求。"
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,270评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,489评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,630评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,906评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,928评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,718评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,442评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,345评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,802评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,984评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,117评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,810评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,462评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,011评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,139评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,377评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,060评论 2 355

推荐阅读更多精彩内容