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閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容