ABP SimpleTaskSystem 翻译

原帖

下载源码

用我的方法配置好你的环境

译者注:必须要下源码,作者文章中的代码比较飘逸,会跳过部分声明,你可能会发现很多类没有定义,要去源码找来看。当然,我会在本文中尽量补充完整,使得这个入门级别的文章更加“入门”

项目截屏

Paste_Image.png

介绍


ASP.NET Boilerplate 是一个开源框架它整合了所有上面提到的框架或类库来使你的开发更容易。它提供了一个很好的开发应用的基础。它原生支持依赖注入领域驱动设计分层体系结构。这个简单的应用同样实现了 数据验证异常处理本地化响应式设计

在这篇文章中,我会来展示如何使用下面的工具来开发一款 单页应用 Single-Page Web Application (SPA)

  • ASP.NET MVCASP.NET Web API 作为网站框架
  • Angularjs 作为 SPA 框架
  • EntityFramework 作为对象关系映射 ORM (Object-Relational Mapping) 框架
  • Castle Windsor 作为依赖注入框架
  • Twitter Bootstrap 作为 HTML/CSS 框架
  • Log4Net 做登录(logging), AutoMapper 做对象映射 (object-to-object mapping).
  • 最后 ASP.NET Boilerplate 作为模板和项目框架

通过ABP模板创建应用


ABP整合并配置好了企业级网站开发用最好的工具,为我们节省了大量时间。

让我们打开 aspnetboilerplate.com/Templates 来创建我们的应用

原帖图片

译者注,现在ABP模板下面多出一个 module zero 是用来创建权限以及用户的,勾和不勾与本文没关系。另外源码中的项目结构也和现在下载的模板有所出入,还多出NHibernate or Durandal。无视就好

我的图片

点击按钮后输入验证码 并开始下载。

不要急着关掉,你需要按照网页提示去创建你的本地sqlserver数据库

Paste_Image.png

就是刚才你给项目起的名字,当然你也可以翻过来修改连接字符串来指定数据库

用之前配置好的 VS2015打开下载的项目,可能会出现如图情况,点击确认。具体配置方法

Paste_Image.png

加载完成后,右键点击项目,然后还原nuget包

把web项目设置为启动项目,然后点击F5运行就能看到网站欢迎页面

Paste_Image.png

创建实体


我会创建一个简单应用来给一些人发一些任务,所以我需要 Task 任务 和 Person 人 2个实体。(注:实体作为业务的一部分,创建在core领域层并单独拥有文件夹,后续还要存放仓库接口和关联文件,如图)

Paste_Image.png

Task 任务,简单定义了一个描述,创建时间和任务的状态。同时也含有一个 被指派人的引用(注:有人发现了,没有定义主键,因为他们都继承自Entity 会默认加一个 int 类型 的字段 叫 Id.这里 Task任务通过泛型修改了id类型为 long)

(注:文件夹默认要用复数来取名,单复数同型的单词比如person,文件夹不要取一样的名字,会导致命名空间和类名重名,无法使用)

public class Task : Entity<long>
{
    [ForeignKey("AssignedPersonId")]
    public virtual Person AssignedPerson { get; set; }

    public virtual int? AssignedPersonId { get; set; }

    public virtual string Description { get; set; }

    public virtual DateTime CreationTime { get; set; }

    public virtual TaskState State { get; set; }

    public Task()
    {
        CreationTime = DateTime.Now;
        State = TaskState.Active;
    }
}

Person 人,简单定义一下名字

public class Person : Entity
{
    public virtual string Name { get; set; }
}

创建 DbContext


众所周知,EF 需要DbContext来工作.我们需要先定义他,ABP模板已经为我们创建了一个DbContext模板,我们只需要在里面添加 IDbSet 就行了

public class SimpleTaskSystemDbContext : AbpDbContext
{
    public virtual IDbSet<Task> Tasks { get; set; }

    public virtual IDbSet<Person> People { get; set; }

    public SimpleTaskSystemDbContext()
        : base("Default")
    {

    }

    public SimpleTaskSystemDbContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
            
    }
}

他使用 Default 连接字符串 , 配置在webconfig中

<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />

创建migration


我们使用EF的code first migrations来升级数据库.ABP模板已经默认打开了migrations,并且有下面这个配置类

internalinternal sealed class Configuration : DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context)
    {
        context.People.AddOrUpdate(
            p => p.Name,
            new Person {Name = "Isaac Asimov"},
            new Person {Name = "Thomas More"},
            new Person {Name = "George Orwell"},
            new Person {Name = "Douglas Adams"}
            );
    }
}

在这个seed方法中 我添加了4个人 来初始化数据,然后打开 包控制台Package Manager Console,选择 EF项目,输入下面的命令

Paste_Image.png
Add-Migration "InitialCreate"

然后

Update-Database

注:可以用tab"自动完成",比如输入update 然后 tab

Paste_Image.png

当我们修改我们的数据实体时,可以很轻松的通过migration来完成数据库升级,想要了解更多?看entity framework的文档

定义仓储接口


在领域驱动设计 (DDD)中,仓储被用来实现具体的数据库访问代码 .ABP 为每一个数据实体创建了一个自动化的通用仓储IRepository接口,IRepository 已经定义了一些常用怎删改查方法,如图

Paste_Image.png

注:当你声明的仓储继承于IRepository,就会自动拥有这些方法

如果需要,我们可以扩展这些仓储,我会继承他来创建一个Task仓储,由于我想要分离接口和实现,所以我先在这里创建接口(注:添加在核心领域层 Tasks文件夹中)

public interface ITaskRepository : IRepository<Task, long>
{
    List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}

它继承于通用的ABP IRepository.所以他可以使用所有IRepository中定义的方法,并且我们可以添加我们自己的方法 GetAllWithPeople(...).

不需要为person创建仓储,因为默认方法就已经够用了.ABP提供了一种注入通用仓储的方法而不需要单独去创建仓储类.我们会在后面的章节(创建应用服务)中看到

我会把仓储接口定义在核心层,因为他是领域/业务的一部分

实现仓储

我们需要实现上面定义过的 ITaskRepository 接口.我会在EF层实现仓储,这样 领域层就完全和数据分离了

当我们创建模板的时候,ABP会默认创建一个仓储类 SimpleTaskSystemRepositoryBase.拥有这样一个基础类是非常好的,我们可以在日后为我们的仓储添加自己想要的通用方法.下面是我实现 TaskRepository 的代码

public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository
{
    public TaskRepository(IDbContextProvider<ABPSimpleTaskTestDbContext> dbContextProvider) : base(dbContextProvider)
        {
        }

        public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
        {

            //在仓储方法中,我们不需要去定义数据库连接 DbContext 和数据事务

            var query = GetAll(); //GetAll() 返回 IQueryable<T>, 我们可以通过它来查询.
            //var query = Context.Tasks.AsQueryable(); //或者, 我们可以直接使用 EF's DbContext .
            //var query = Table.AsQueryable(); //再者: 我们可以直接使用 'Table' 属性来代替'Context.Tasks', 他们完全相同.

            //添加了一下where过滤...

            if (assignedPersonId.HasValue)
            {
                query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value);
            }

            if (state.HasValue)
            {
                query = query.Where(task => task.State == state);
            }

            return query
                .OrderByDescending(task => task.CreationTime)
                .Include(task => task.AssignedPerson) //一起查询被分配任务的人
                .ToList();
        }
}

(注:删掉 using System.Threading.Tasks;)

TaskRepository 继承于 SimpleTaskSystemRepositoryBase 并且实现了我们刚才定义的 ITaskRepository 接口

GetAllWithPeople 是我们定义的 可以添加条件并拿到关联实体 的查询任务的方法.另外, 我们可以自由的在仓储中使用 Context (EF的 DBContext) 和 数据库 . ABP会为我们管理数据库连接,事务,创建和回收DbContext. (从 文档 了解更多)

创建应用服务

应用服务 通过提供 外观样式方法 从 领域层 中分离出 演示层 .我会定义应用服务在应用程序集(Application)中.首先,我会定义一个task的应用服务接口

注:原文在这里跳过了很重要的内容,Dto的相关介绍,译者这里靠自己的理解补上

和其他层一样,为tasks创建一个文件夹.并且创建好Dtos文件夹 Dto简介

Paste_Image.png

译者目前无法完全表述清楚这些文件的意思,所以从源码中复制一下吧,毕竟我们这次的目的是ABP入门

创建ITaskAppService 接口

public interface ITaskAppService : IApplicationService
{
    GetTasksOutput GetTasks(GetTasksInput input);
    void UpdateTask(UpdateTaskInput input);
    void CreateTask(CreateTaskInput input);
}

ITaskAppService 继承于 IApplicationService. 这样, ASP.NET Boilerplate 可以自动为这个类提供一些特征 (比如依赖注入和数据验证). 现在,让我们来实现 ITaskAppService:

    /// <summary>
    /// 实现 <see cref="ITaskAppService"/> 来执行task相关的应用服务功能
    /// 
    /// 继承 <see cref="ApplicationService"/>.
    /// <see cref="ApplicationService"/> 包括一些常用的应用服务 (比如登陆和本地化).
    /// </summary>
    public class TaskAppService : ApplicationService, ITaskAppService
    {
        //这些在构造函数中的成员使用了构造函数注入

        private readonly ITaskRepository _taskRepository;
        private readonly IRepository<Person> _personRepository;

        /// <summary>
        /// 在构造函数中,我们可以获得所需要的类和接口
        /// 他们被依赖注入系统自动送到这里
        /// </summary>
        public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
        {
            _taskRepository = taskRepository;
            _personRepository = personRepository;
        }

        public GetTasksOutput GetTasks(GetTasksInput input)
        {
            //调用特殊的仓储方法 GetAllWithPeople 
            var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);

            //使用 AutoMapper 自动转换 List<Task> 到 List<TaskDto>.
            return new GetTasksOutput
            {
                Tasks = Mapper.Map<List<TaskDto>>(tasks)
            };
        }

        public void UpdateTask(UpdateTaskInput input)
        {
            //我们可以使用日志(Logger),它已经在基础类(ApplicationService)里定义过了
            Logger.Info("Updating a task for input: " + input);
            
            //通过id获取实体可以用仓储中标准的Get方法
            var task = _taskRepository.Get(input.TaskId);

            //从接收到的task实体来修改属性

            if (input.State.HasValue)
            {
                task.State = input.State.Value;
            }

            if (input.AssignedPersonId.HasValue)
            {
                task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
            }
            
            //我们甚至不需要调用仓储中的update方法
            //因为每一个应用服务方法都是一个单元为默认范围的
            //ABP自动保存所有的修改,当一个工作单元结束的时候(没有任何例外)
        }

        public void CreateTask(CreateTaskInput input)
        {
            //我们可以使用日志(Logger),它已经在基础类(ApplicationService)里定义过了
            Logger.Info("Creating a task for input: " + input);

            //用收到的input里的值新建一个task
            var task = new Task { Description = input.Description };

            if (input.AssignedPersonId.HasValue)
            {
                task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
            }

            //使用仓储中标准饿Insert方法来保存task
            _taskRepository.Insert(task);
        }
    }

注:这里用到了automapper,而这个东西是需要配置的,到源文件里去找到下面2个文件,进行配置

Paste_Image.png
static class DtoMappings
    {
        public static void Map(IMapperConfigurationExpression mapper)
        {
            mapper.CreateMap<Task, TaskDto>();
        }
    }
[DependsOn(typeof(ABPSimpleTaskTestCoreModule), typeof(AbpAutoMapperModule))]
    public class ABPSimpleTaskTestApplicationModule : AbpModule
    {
        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());

            //为了使用automapper我们必须在这里先声明
            Configuration.Modules.AbpAutoMapper().Configurators.Add(mapper =>
            {
                DtoMappings.Map(mapper);
            });
        }
    }

TaskAppService 使用仓储来进行数据库操作. 它通过 构造函数注入(constructor injection) 的方式,在他的构造函数中提供了引用. ASP.NET Boilerplate 原生实现了依赖注入, 所以我们可以自由使用构造注入或者属性注入 (了解更多 ASP.NET Boilerplate 中的依赖注入 文档).

注意, 我们通过注入IRepository<Person>来使用 PersonRepository. ASP.NET Boilerplate 就会自动为我们创建仓储. 如果 IRepository 中的默认方法够我们使用, 那我们就没有必要去创建仓储类.
应用服务方法需要用到数据传输对象 -- Data Transfer Objects (DTOs). 这是非常好的,而且我很建议使用这样的模式. 但是你也不一定要照做,只要你能解决这些暴露实体到演示层的问题

GetTasks 方法中, 我使用了之前实现过的 GetAllWithPeople 方法 。 它返回了一个 List<Task> ,但是我希望返回 List<TaskDto> 到演示层. AutoMapper 帮助我们自动转换 Task 对象到 TaskDto 对象. GetTasksInput 和 GetTasksOutput 是特别为 GetTasks 方法而定义的数据传输对象 (DTOs) .
UpdateTask 方法中, 我从数据库获得 Task (使用IRepository's 的 Get 方法) 并且修改了 Task的值. 注意,你没有调用过仓储中的 Update 方法 . ASP.NET Boilerplate 实现了 UnitOfWork 模式. 所以, 所有在一个应用服务里的修改就是一个 unit of work (atomic原子的) 并且在方法的最后自动提交到数据库.

CreateTask 方法,我简单地创建了一个 Task 并且使用 IRepository自带的 Insert 方法添加到数据库 .

ASP.NET Boilerplate 中的 ApplicationService 类 有一些功能来帮助我们更容易地开发应用.比如, 它定义的 Logger 来做日志功能. 所以, 我们可以在这里让 TaskAppService 继承 ApplicationService 并且使用它的 Logger 日志功能. 我们任然可以选择性地去继承这个类,只要你实现 IApplicationService 接口(注意 ITaskAppService接口 继承自 IApplicationService接口).

验证

ABP自动在 application service 验证 inputs .CreateTask 方法获取了 CreateTaskInput 作为参数

public class CreateTaskInput
{
    public int? AssignedPersonId { get; set; }

    [Required]
    public string Description { get; set; }
}

这里 Description 被标记成 [Required] 如果你想使用一些常规验证,你可以使用 Data Annotation attributes中所有的验证.你可以实现接口 ICustomValidate 就像我 在 UpdateTaskInput 中实现的一样 :

public class UpdateTaskInput : ICustomValidate
{
    [Range(1, long.MaxValue)]
    public long TaskId { get; set; }

    public int? AssignedPersonId { get; set; }

    public TaskState? State { get; set; }

    public void AddValidationErrors(List<ValidationResult> results)
    {
        if (AssignedPersonId == null && State == null)
        {
            results.Add(new ValidationResult("Both of AssignedPersonId and State can not be null in order to update a Task!", new[] { "AssignedPersonId", "State" }));
        }
    }

    public override string ToString()
    {
        return string.Format("[UpdateTask > TaskId = {0}, AssignedPersonId = {1}, State = {2}]", TaskId, AssignedPersonId, State);
    }
}

AddValidationErrors 方法 可以写你自己的验证方法代码

异常处理

有人注意到了,我们还没有处理过任何异常,ABP自动为我们处理异常,记录日志并给客户端返回一个适当的错误.在客户端可以看到错误信息.事实上,我这个是为了ASP.NET MVC and Web API Controller actions 准备的,由于我们会用WEBAPI来暴露 TaskAppService 所以我们不需要处理异常,可以看 exception handling 文档 获得更多信息

构建web api services

我想要暴露我的服务到远程客户端,这样,我的 AngularJs 就可以使用ajax轻松调用他们.

ABP提供一个自动化方式来 把 服务方法 转换成webapi .我只要使用 DynamicApiControllerBuilder 就像下面:

DynamicApiControllerBuilder
    .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
    .Build();

在这个例子中,ABP 在 Application 层的应用程序中 找到了所有继承 IApplicationService 的 接口 ,并且为每一个 application service class 创建了一个 web api controller.There are alternative syntaxes for fine control. 我们接下来会看到如何使用ajax来调用这些服务

开发 SPA

我会实现一个SPA 来作为我项目中的用户界面 ,Angularjs 是使用最广的 SPA框架

ABP提供了一个模板 可以很轻松地使用 AngularJs . 这个模板有2个页面 Home 和 About .Bootstrap 作为 HTML/CSS 框架 同事本地化加入了 英语和 土耳其语 在 ABP的本地化系统中 (你可以很轻松地添加一个新语言或者移除它)

我们首先来修改路由,ABP使用 AngularUI-Router the de-facto standard router of AngularJs ,It provides state based routing modal,我们有2个 视图 task list 和 new task ,所以我们修改 app.js 中的路由设置 如下:

app.config([
    '$stateProvider', '$urlRouterProvider',
    function ($stateProvider, $urlRouterProvider) {
        $urlRouterProvider.otherwise('/');
        $stateProvider
            .state('tasklist', {
                url: '/',
                templateUrl: '/App/Main/views/task/list.cshtml',
                menu: 'TaskList' //Matches to name of 'TaskList' menu in SimpleTaskSystemNavigationProvider
            })
            .state('newtask', {
                url: '/new',
                templateUrl: '/App/Main/views/task/new.cshtml',
                menu: 'NewTask' //Matches to name of 'NewTask' menu in SimpleTaskSystemNavigationProvider
            });
    }
]);

app.js

app.js 是主要的 js文件 用来启动和设置我们的SPA,需要注意的是 我们正在使用cshtml 文件来作为视图,通常情况下,html文件会被作为AngularJs的视图 .ABP经过处理 使得cshtml也能使用AngularJs.这样 我们就能使用razor来构成我们的html了

ABP基础插件 来创建和显示菜单,他允许我们在C#中定义菜单但可以同时在C#和JS中使用,看 **SimpleTaskSystemNavigationProvider ** 类 它创建了菜单 ,看 header.js/header.cshtml 通过angular 来显示菜单

接下来 首先我要 为了 task list 视图 创建一个 Angular controller :

(function() {
    var app = angular.module('app');

    var controllerId = 'sts.views.task.list';
    app.controller(controllerId, [
        '$scope', 'abp.services.tasksystem.task',
        function($scope, taskService) {
            var vm = this;

            vm.localize = abp.localization.getSource('SimpleTaskSystem');

            vm.tasks = [];

            $scope.selectedTaskState = 0;

            $scope.$watch('selectedTaskState', function(value) {
                vm.refreshTasks(); //当selectedTaskState改变时调用
            });

            vm.refreshTasks = function() {
                abp.ui.setBusy( //设置页面为忙碌 直到 getTasks 方法完成
                    null,
                    taskService.getTasks({ //直接从 javascript 调用 application service 方法
                        state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null
                    }).success(function(data) {
                        vm.tasks = data.tasks;
                    })
                );
            };

            vm.changeTaskState = function(task) {
                var newState;
                if (task.state == 1) {
                    newState = 2; //Completed
                } else {
                    newState = 1; //Active
                }

                taskService.updateTask({
                    taskId: task.id,
                    state: newState
                }).success(function() {
                    task.state = newState;
                    abp.notify.info(vm.localize('TaskUpdatedMessage'));
                });
            };

            vm.getTaskCountText = function() {
                return abp.utils.formatString(vm.localize('Xtasks'), vm.tasks.length);
            };
        }
    ]);
})();

我用 'sts.views.task.list' 为控制器命名 这个是我的习惯,你也可以简单地起名叫 'ListController' ,AngularJs同样使用依赖注入,我们在这里注入了 $scope 和 abp.services.tasksystem.task . 前者是Angular的 局部变量,后者是一个自动创建的 ITaskAppService 的js服务代理(我们之前在 构建web api services 中设置过)

ABP提供了一些基础插件让 服务器和客户端 来使用 统一的 localization 本地化文本

vm.tasks 是一个 tasks的集合 会被现实到视图中 vm.refreshTasks 方法 会调用 taskService 来填充这个集合,他会在 selectedTaskState改变时被调用 (用 $scope.$watch 来监听 )

正如你所见,调用一个服务器方法 是非常容易且直接了当的 . 这就是ABP的特色,他生成 web api 层 和 与之通行的 js 代理层 ,这样 我们就能够像调用js方法一样调用服务端方法了.而且它完全集成在 AngularJs 中 (使用了 Angular's $http service)

让我们接着来看task list 视图

<div class="panel panel-default" ng-controller="sts.views.task.list as vm">

    <div class="panel-heading" style="position: relative;">
        <div class="row">
            
            <!-- Title -->
            <h3 class="panel-title col-xs-6">
                @L("TaskList") - <span>{{vm.getTaskCountText()}}</span>
            </h3>
            
            <!-- Task state combobox -->
            <div class="col-xs-6 text-right">
                <select ng-model="selectedTaskState">
                    <option value="0">@L("AllTasks")</option>
                    <option value="1">@L("ActiveTasks")</option>
                    <option value="2">@L("CompletedTasks")</option>
                </select>
            </div>
        </div>
    </div>

    <!-- Task list -->
    <ul class="list-group" ng-repeat="task in vm.tasks">
        <div class="list-group-item">
            <span class="task-state-icon glyphicon" ng-click="vm.changeTaskState(task)" ng-class="{'glyphicon-minus': task.state == 1, 'glyphicon-ok': task.state == 2}"></span>
            <span ng-class="{'task-description-active': task.state == 1, 'task-description-completed': task.state == 2 }">{{task.description}}</span>
            <br />
            <span ng-show="task.assignedPersonId > 0">
                <span class="task-assignedto">{{task.assignedPersonName}}</span>
            </span>
            <span class="task-creationtime">{{task.creationTime}}</span>
        </div>
    </ul>

</div>

第一行的 ng-controller 属性 绑定了 视图的控制器 @L("TaskList") 为 "task list" 提供了本地化的文字翻译 (在服务端生成html时),这全归功于它是一个cshtml文件

ng-model绑定了 下拉框的变量,当变量改变的时候 下拉框自动改变 同样,当下拉框改变的时候 变量也会被保存,这个是 AngularJs 实现的双向绑定

ng-repeat
是另外一个 Angular 的指令 用来循环集合 生成html ,当 集合改变的时候 (比如添加了一个内容) 它会自动 反射到视图上,这个是AngularJs另外一个强大的功能

注意 当你添加一个 js文件 ,你需要添加它到你的页面中

本地化

ABP提供了一个灵活且强大的 本地化系统 你可以使用xml文件 或者 Resource 文件 来作为 数据源 你可以定制 本地化数据源 看 documentation 获得更多,我使用XML文件(在web项目中 的 Localization 文件夹)

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

推荐阅读更多精彩内容