beego源碼學習-ORM,SQL解釋器


配置ORM

type ORMdemoController struct {
    beego.Controller
}

func (this * ORMdemoController) Get(){
    //注冊數據驅動
    orm.RegisterDriver("mysql", orm.DRMySQL) // mysql、sqlite3、postgres 這三種是beego默認已經注冊過的,所以可以無需設置
    //注冊數據庫 ORM 必須注冊一個別名為 default 的數據庫,作為默認使用
    //五個參數:1、數據庫別名;2、數據庫驅動;3、數據庫賬戶:密碼@鏈接地址/數據庫名稱;4、最大空閑連接數;5、最大數據連接
    orm.RegisterDataBase("default", "mysql", "root:passwd@tcp(127.0.0.1:3306)/db_user?charset=utf8")
    //設置數據庫時區
    //orm.DefaultTimeLoc = time.UTC
    //注冊模型
    orm.RegisterModel(new(UserTable))
    //自動創建表:1、默認數據;2、是否開啟創建表;3、是否更新表
    orm.RunSyncdb("default", true, true)
    this.Ctx.WriteString("表創建成功")
}

創建Model

  • 定義model時注意事項

1、我們定義結構體作為表,必須要有主鍵。

2、當 Field 類型為 int, int32, int64, uint, uint32, uint64 時,可以設置字段為自增健。

3、當模型定義里沒有主鍵時,符合int類型且名稱為 Id 的 Field 將被視為自增健。

4、屬性的首字母最好是大寫,設置屬性為公開訪問性。

5、未定義其他規則時,自動生表時,所有字段都為NOT NULL,id為自增主鍵,其他都有其類型默認值

type UserTable struct {
    User     int //添加 int型字段ID,作為主鍵
    Pwd      string
    RealName string
    Age      string
    IdCard   string
    Email    string
    Tel      string
}
  • 使用結構體tag進行表的詳細屬性設置
type UserTable struct {
    ID       int    `orm:"pk;auto;column(id)"`        //設置主鍵自增長 字段名為 id
    User     string `orm:"size(15);column(user)"`     //設置長度為15 字段名為 user
    Pwd      string `orm:"size(20);column(pwd)"`      //設置長度為20 字段名為 pwd
    RealName string `orm:"size(10);column(realname)"` //設置長度為10 字段名為 realname
    Age      string `orm:column(age)"`                //設置字段名為age
    IdCard   string `orm:"size(18);column(idcard)"`   //設置長度為18 字段名為 idcard
    Email    string `orm:"size(100);column(email)"`   //設置長度為100 字段名為 email
    Tel      string `orm:"size(11);column(tel)"`      //設置長度為11 字段名為 tel
}
  • 表名

beego中表名默認使用駝峰:UserTable

遇到大寫會增加 _,原名稱中的下劃線保留:UserTable->user_table

//自定義表名
func (u *UserTable) TableName() string {
    return "user"  //表名被改為user
}

//使用RegisterModelWithPrefix為表名設置前綴:prefix_user_table
orm.RegisterModelWithPrefix("prefix_", new(UserTable)) 
  • 關聯關系定義
//一對一,rel(one): 創建對應關系的字段:表_id,即對應主鍵,有唯一約束
type User struct {
    ......
    Profile     *Profile   `orm:"rel(one)"` 
}

//反向一對一,reverse(one): 不會創建字段,可選tag
type Profile struct {
    ......
    User        *User   `orm:"reverse(one)"` 
}

//一對多,rel(fk): 創建對應關系的字段:表名_id,即對應主鍵,沒有約束
type Post struct {
    ......
    User  *User  `orm:"rel(fk)"`
}

//反向一對多,reverse(many): 不會創建字段,寫在關系為多的類里
type User struct {
    ......
    Post        []*Post `orm:"reverse(many)"` 
}

//多對多,rel(m2m): 不會創建字段,聲明處的表名,為自動創建的表的表名前綴(post)
type Post struct {
    ......
    Tags  []*Tag `orm:"rel(m2m)"`
}

//反向多對多,reverse(many): 不會創建字段,聲明處的表名,為自動創建的表的表名后綴+s(tags)
type Tag struct {
    ......
    Posts []*Post `orm:"reverse(many)"` 
}

ps:反向一對多和反向多對多,關鍵詞一樣


ORM的使用

  • 一些基本操作
type Ormer interface {
    Read(interface{}, …string) error
    ReadOrCreate(interface{}, string, …string) (bool, int64, error)
    Insert(interface{}) (int64, error)
    InsertMulti(int, interface{}) (int64, error)
    Update(interface{}, …string) (int64, error)
    Delete(interface{}) (int64, error)
    LoadRelated(interface{}, string, …interface{}) (int64, error)
    QueryM2M(interface{}, string) QueryM2Mer
    QueryTable(interface{}) QuerySeter
    Using(string) error
    Begin() error
    Commit() error
    Rollback() error
    Raw(string, …interface{}) RawSeter
    Driver() Driver
}
  • 基本操作符
  • 基本的CRUD
type UserTable struct {......}

func Create(param interface{}) (int, error) {
    return orm.NewOrm().Insert(param)
}
func Update(param interface{}, fields ...string) (int, error) {
    return orm.NewOrm().Update(param, fields...)
}
func Delete(param interface{}, cols ...string) (int, error) {
    return orm.NewOrm().Delete(param, cols...)
}
func Read(md interface{}, cols ...string) error {
    return orm.NewOrm().Read(md, cols...)
}

ORM源碼分析

主要流程

  1. 啟動應用時,完成orm相關配置的注冊:數據庫、model、rel

  2. 使用時,實例化orm對象、關聯關系處理、進行sql解析

  • 注冊ORM相關配置
    //注冊數據庫 ORM 必須注冊一個別名為 default 的數據庫,作為默認使用
    //五個參數:1、數據庫別名;2、數據庫驅動;3、數據庫賬戶:密碼@鏈接地址/數據庫名稱;4、最大空閑連接數;5、最大數據連接
    orm.RegisterDataBase("default", "mysql", "root:passwd@tcp(127.0.0.1:3306)/db_user?charset=utf8")
    //注冊模型
    orm.RegisterModel(new(UserTable))
    //自動創建表:1、默認數據;2、是否開啟創建表;3、是否更新表
    orm.RunSyncdb("default", true, true)

注冊操作做了啥

  • 注冊db
//使用當前驅動的連接配置信息(數據庫賬戶:密碼@鏈接地址/數據庫名稱),設置數據庫連接參數
func RegisterDataBase(aliasName, driverName, dataSource string, params ...int) error {
    ......
}
  • 注冊model
//===== github.com/astaxie/beego/orm/orm.go =====

func RegisterModel(models ...interface{}) {
    ......
    RegisterModelWithPrefix("", models...)
}
func RegisterModelWithPrefix(prefix string, models ...interface{}) {
    ......
    for _, model := range models {
        registerModel(prefix, model, true)
    }
}
//RegisterModel 和 RegisterModelWithPrefix 都是對 registerModel 的封裝
func registerModel(PrefixOrSuffix string, model interface{}, isPrefix bool) {
    //通過反射獲取模型信息
    val := reflect.ValueOf(model)
    typ := reflect.Indirect(val).Type()
    ......
    //處理表名和前綴,完整路徑
    table := getTableName(val)
    if PrefixOrSuffix != "" {
        if isPrefix {
            table = PrefixOrSuffix + table
        } else {
            table = table + PrefixOrSuffix
        }
    }
    // models's fullname is pkgpath + struct name
    name := getFullName(typ)
    if _, ok := modelCache.getByFullName(name); ok {
        fmt.Printf("<orm.RegisterModel> model `%s` repeat register, must be unique\n", name)
        os.Exit(2)
    }

    if _, ok := modelCache.get(table); ok {
        fmt.Printf("<orm.RegisterModel> table name `%s` repeat register, must be unique\n", table)
        os.Exit(2)
    }

    //通過反射判斷是否有id字段,設置主鍵
    mi := newModelInfo(val)
    if mi.fields.pk == nil {
    outFor:
        for _, fi := range mi.fields.fieldsDB {
            if strings.ToLower(fi.name) == "id" {
                switch fi.addrValue.Elem().Kind() {
                case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
                    fi.auto = true
                    fi.pk = true
                    mi.fields.pk = fi
                    break outFor
                }
            }
        }

        if mi.fields.pk == nil {
            fmt.Printf("<orm.RegisterModel> `%s` needs a primary key field, default is to use 'id' if not set\n", name)
            os.Exit(2)
        }

    }
    //處理完成的model信息,設置到model緩存中
    mi.table = table
    mi.pkg = typ.PkgPath()
    mi.model = model
    mi.manual = true

    modelCache.set(table, mi)
}

ORM初始化過程

以這樣一段orm操作作為示例:

func main() {
    o := orm.NewOrm()
    user := User{Name: "user"}
    u := User{Id: user.Id}
    err = o.Read(&u)
    fmt.Println(err)
}
  • 初始化orm

NewOrm時,先調用BootStrap方法完成模型信息的加載,然后調用Using選中默認數據庫:

源碼github.com/astaxie/beego/orm/orm.go

type orm struct {
    alias *alias
    db    dbQuerier
    isTx  bool
}

func NewOrm() Ormer {
    //orm的構造函數一開始就會執行啟動引導
    BootStrap() // execute only once

    //選擇一個默認的數據庫驅動
    o := new(orm)
    err := o.Using("default")
    if err != nil {
        panic(err)
    }
    return o
}

BootStrap方法:加載緩存中的model實例,把model中定義的字段跟數據表中的字段進行綁定關聯,無緩存會進行首次初始化。

Using方法:讀取緩存的數據庫信息,賦值給 orm 的 alias 屬性,之后就可以進行正常的crud操作了

//====== orm/orm.go ======
func (o *orm) Using(name string) error {
    ......
    //將數據庫緩存信息賦值給 orm 的 alias 屬性
    if al, ok := dataBaseCache.get(name); ok {
        o.alias = al
        if Debug {//debug模式下開啟sql日志
            o.db = newDbQueryLog(al, al.DB)
        } else {
            o.db = al.DB
        }
    } else {
        return fmt.Errorf("<Ormer.Using> unknown db alias name `%s`", name)
    }
    return nil
}

//============ orm/models_boot.go ============
func BootStrap() {
    //對model的操作緩存進行加鎖,保證只會執行一次
    modelCache.Lock()
    defer modelCache.Unlock()
    if modelCache.done {
        return
    }
    bootStrap()
    modelCache.done = true
}

func bootStrap() {
    ......
    var (
        err    error
        models map[string]*modelInfo
    )
    
    //遍歷模型的字段
    models = modelCache.all()
    for _, mi := range models {
        for _, fi := range mi.fields.columns {
            //如果有設置rel或者reverse關系
            if fi.rel || fi.reverse {
                elm := fi.addrValue.Type().Elem()
                if fi.fieldType == RelReverseMany || fi.fieldType == RelManyToMany {
                    elm = elm.Elem()
                }
                //檢查關聯的模型是否注冊,并獲取模型信息
                name := getFullName(elm)
                mii, ok := modelCache.getByFullName(name)
                if !ok || mii.pkg != elm.PkgPath() {
                    err = fmt.Errorf("can not find rel in field `%s`, `%s` may be miss register", fi.fullName, elm.String())
                    goto end
                }
                fi.relModelInfo = mii

                switch fi.fieldType {
                case RelManyToMany:
                    //如果是多對多關系,且聲明了中間表,則根據中間表獲取關聯表信息;并為當前字段設置模型關聯信息
                    if fi.relThrough != "" {
                        if i := strings.LastIndex(fi.relThrough, "."); i != -1 && len(fi.relThrough) > (i+1) {
                            pn := fi.relThrough[:i]
                            rmi, ok := modelCache.getByFullName(fi.relThrough)
                            if !ok || pn != rmi.pkg {
                                err = fmt.Errorf("field `%s` wrong rel_through value `%s` cannot find table", fi.fullName, fi.relThrough)
                                goto end
                            }
                            fi.relThroughModelInfo = rmi
                            fi.relTable = rmi.table
                        } else {
                            err = fmt.Errorf("field `%s` wrong rel_through value `%s`", fi.fullName, fi.relThrough)
                            goto end
                        }
                    } else {
                        //未主動聲明中間表關系,創建新的關聯模型實例,判斷關聯表是否被注冊
                        i := newM2MModelInfo(mi, mii)
                        if fi.relTable != "" {
                            i.table = fi.relTable
                        }
                        if v := modelCache.set(i.table, i); v != nil {
                            err = fmt.Errorf("the rel table name `%s` already registered, cannot be use, please change one", fi.relTable)
                            goto end
                        }
                        fi.relTable = i.table
                        fi.relThroughModelInfo = i
                    }

                    fi.relThroughModelInfo.isThrough = true
                }
            }
        }
    }

    //后面是一些模板代碼
    //遍歷字段關聯,自動生成字段反向關聯信息
    models = modelCache.all()
    for _, mi := range models {
        for _, fi := range mi.fields.fieldsRel {......}
    }
    //遍歷字段關聯,檢查反向多對多關聯關系設置
    models = modelCache.all()
    for _, mi := range models {
        for _, fi := range mi.fields.fieldsRel {......}
    }
    //遍歷字段反向關聯,檢查字段是否在模型中設置
    models = modelCache.all()
    for _, mi := range models {
        for _, fi := range mi.fields.fieldsReverse {.....}
    }
}
  • 當我們使用QueryTable設置表時
func (o *orm) QueryTable(ptrStructOrTableName interface{}) (qs QuerySeter) {
    var name string
    if table, ok := ptrStructOrTableName.(string); ok {
        //如果是指針結構體或表名,根據命名策略,到模型緩存中獲取模型
        name = nameStrategyMap[defaultNameStrategy](table)
        if mi, ok := modelCache.get(name); ok {
            qs = newQuerySet(o, mi)
        }
    } else {
        //否則,通過反射獲取到表名,再根據表名獲取緩存中的模型
        name = getFullName(indirectType(reflect.TypeOf(ptrStructOrTableName)))
        if mi, ok := modelCache.getByFullName(name); ok {
            qs = newQuerySet(o, mi)
        }
    }
    //最后返回一個QuerySet
    return
}
  • QuerySeter

QuerySeter接口定義了一系列的查詢方法:

//====== orm/types.go ======
type QuerySeter interface {
    // 篩選條件 where 
    Filter(string, ...interface{}) QuerySeter
    // 原生過濾語句
    // qs.FilterRaw("user_id IN (SELECT id FROM profile WHERE age>=18)")
    FilterRaw(string, string) QuerySeter
    // 排除篩選條件 where not in
    Exclude(string, ...interface{}) QuerySeter
    // 設置單個篩選條件
    SetCond(*Condition) QuerySeter
    // 獲取指定的篩選條件
    GetCond() *Condition
    // 分頁
    Limit(limit interface{}, args ...interface{}) QuerySeter
    Offset(offset interface{}) QuerySeter
    // 分組
    GroupBy(exprs ...string) QuerySeter
    // 排序
    OrderBy(exprs ...string) QuerySeter
    // 模型關聯查詢
    RelatedSel(params ...interface{}) QuerySeter
    // 去重
    Distinct() QuerySeter
    // 給構造器設置FOR UPDATE
    ForUpdate() QuerySeter
    // 計數
    Count() (int64, error)
    // 是否存在
    Exist() bool
    // 更新
    Update(values Params) (int64, error)
    // 刪除
    Delete() (int64, error)
    // 返回一個插入queryer
    PrepareInsert() (Inserter, error)
    // 查詢多條數據
    All(container interface{}, cols ...string) (int64, error)
    // 查詢單條數據
    One(container interface{}, cols ...string) error
    // 查詢多條結果并將結果存入字符串類型的map指針變量中
    Values(results *[]Params, exprs ...string) (int64, error)
    // 查詢多條結果并將結果存入接口類型的map指針變量中
    ValuesList(results *[]ParamsList, exprs ...string) (int64, error)
    // 將所有結果集存入map變量中,沒有字段名
    ValuesFlat(result *ParamsList, expr string) (int64, error)
    // ptrStruct存放查詢結果的指針map變量, keyCol查詢字段名,valueCol字段值
    RowsToMap(result *Params, keyCol, valueCol string) (int64, error)
    // ptrStruct存放查詢結果的結構體指針變量, keyCol查詢字段名,valueCol字段值
    RowsToStruct(ptrStruct interface{}, keyCol, valueCol string) (int64, error)
}
  • QuerySeter接口的實現
//====== orm/orm_queryset.go ======
type querySet struct {
    mi         *modelInfo
    cond       *Condition
    related    []string
    relDepth   int
    limit      int64
    offset     int64
    groups     []string
    orders     []string
    distinct   bool
    forupdate  bool
    orm        *orm
    ctx        context.Context
    forContext bool
}

func newQuerySet(orm *orm, mi *modelInfo) QuerySeter {
    o := new(querySet)
    o.mi = mi
    o.orm = orm
    return o
}

//這里就以 All 方法的實現說明一下,其他的類似
func (o *querySet) All(container interface{}, cols ...string) (int64, error) {
    //All最終會調用DbBaser接口提供的ReadBatch方法
    return o.orm.alias.DbBaser.ReadBatch(o.orm.db, o, o.mi, o.cond, container, o.orm.alias.TZ, cols)
}
  • dbBaser 接口
//====== orm/types.go ======
type dbBaser interface {
    Read(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string, bool) error
    Insert(dbQuerier, *modelInfo, reflect.Value, *time.Location) (int64, error)
    InsertOrUpdate(dbQuerier, *modelInfo, reflect.Value, *alias, ...string) (int64, error)
    InsertMulti(dbQuerier, *modelInfo, reflect.Value, int, *time.Location) (int64, error)
    InsertValue(dbQuerier, *modelInfo, bool, []string, []interface{}) (int64, error)
    InsertStmt(stmtQuerier, *modelInfo, reflect.Value, *time.Location) (int64, error)
    Update(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string) (int64, error)
    Delete(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string) (int64, error)
    ReadBatch(dbQuerier, *querySet, *modelInfo, *Condition, interface{}, *time.Location, []string) (int64, error)
    ......
}
  • DbBaser 接口 ReadBatch 方法的實現
//====== orm/db.go ======
//ReadBatch方法的實現
func (d *dbBase) ReadBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condition, container interface{}, tz *time.Location, cols []string) (int64, error) {

    val := reflect.ValueOf(container)
    ind := reflect.Indirect(val)

    errTyp := true
    one := true
    isPtr := true
    
    //通過反射判斷容器類型,標記是否指針類型
    if val.Kind() == reflect.Ptr {
        fn := ""
        if ind.Kind() == reflect.Slice {
            one = false
            typ := ind.Type().Elem()
            switch typ.Kind() {
            case reflect.Ptr:
                fn = getFullName(typ.Elem())
            case reflect.Struct:
                isPtr = false
                fn = getFullName(typ)
            }
        } else {
            fn = getFullName(ind.Type())
        }
        errTyp = fn != mi.fullName
    }

    if errTyp {
        if one {
            panic(fmt.Errorf("wrong object type `%s` for rows scan, need *%s", val.Type(), mi.fullName))
        } else {
            panic(fmt.Errorf("wrong object type `%s` for rows scan, need *[]*%s or *[]%s", val.Type(), mi.fullName, mi.fullName))
        }
    }
    //從querySeter中獲取分頁信息
    rlimit := qs.limit
    offset := qs.offset

    Q := d.ins.TableQuote()

    var tCols []string
    if len(cols) > 0 {
        //判斷模型是否有關聯關系
        hasRel := len(qs.related) > 0 || qs.relDepth > 0
        tCols = make([]string, 0, len(cols))
        var maps map[string]bool
        if hasRel {
            maps = make(map[string]bool)
        }
        //從模型信息獲取所有字段
        for _, col := range cols {
            if fi, ok := mi.fields.GetByAny(col); ok {
                tCols = append(tCols, fi.column)
                if hasRel {
                    maps[fi.column] = true
                }
            } else {
                return 0, fmt.Errorf("wrong field/column name `%s`", col)
            }
        }
        //如果有關聯關系,從模型信息中讀取關聯字段,并賦值給tCols,完成數據庫字段映射
        if hasRel {
            for _, fi := range mi.fields.fieldsDB {
                if fi.fieldType&IsRelField > 0 {
                    if !maps[fi.column] {
                        tCols = append(tCols, fi.column)
                    }
                }
            }
        }
    } else {
        tCols = mi.fields.dbcols
    }

    colsNum := len(tCols)
    sep := fmt.Sprintf("%s, T0.%s", Q, Q)
    sels := fmt.Sprintf("T0.%s%s%s", Q, strings.Join(tCols, sep), Q)

    //初始dbTables對象,解析關聯關系
    tables := newDbTables(mi, d.ins)
    tables.parseRelated(qs.related, qs.relDepth)

    //構建sql語句各節點:where、groupBy、limit、join
    //后續深入sql組裝
    where, args := tables.getCondSQL(cond, false, tz)
    groupBy := tables.getGroupSQL(qs.groups)
    orderBy := tables.getOrderSQL(qs.orders)
    limit := tables.getLimitSQL(mi, offset, rlimit)
    join := tables.getJoinSQL()

    for _, tbl := range tables.tables {
        if tbl.sel {
            colsNum += len(tbl.mi.fields.dbcols)
            sep := fmt.Sprintf("%s, %s.%s", Q, tbl.index, Q)
            sels += fmt.Sprintf(", %s.%s%s%s", tbl.index, Q, strings.Join(tbl.mi.fields.dbcols, sep), Q)
        }
    }

    //組裝sql
    sqlSelect := "SELECT"
    if qs.distinct {
        sqlSelect += " DISTINCT"
    }
    query := fmt.Sprintf("%s %s FROM %s%s%s T0 %s%s%s%s%s", sqlSelect, sels, Q, mi.table, Q, join, where, groupBy, orderBy, limit)

    if qs.forupdate {
        query += " FOR UPDATE"
    }

    d.ins.ReplaceMarks(&query)

    var rs *sql.Rows
    var err error
    if qs != nil && qs.forContext {
        rs, err = q.QueryContext(qs.ctx, query, args...)
        if err != nil {
            return 0, err
        }
    } else {
        rs, err = q.Query(query, args...)
        if err != nil {
            return 0, err
        }
    }

    refs := make([]interface{}, colsNum)
    for i := range refs {
        var ref interface{}
        refs[i] = &ref
    }

    defer rs.Close()

    slice := ind

    var cnt int64
    //如果存在下一個結果行
    //通過反射獲取字段的類型信息,并設置給data結構體的屬性
    //遍歷dbTables實例,循環tables和models
    //通過反射獲取字段發名稱、值、索引
    for rs.Next() { 
        //if嵌套看著頭大,就不傷害大家的眼睛了
        if one && cnt == 0 || !one {......}
    }

    //如果存在數據行,將slice賦值給ind,否則根據ind的類型創建一個空切片并賦值
    if !one {
        if cnt > 0 {
            ind.Set(slice)
        } else {
            // when a result is empty and container is nil
            // to set a empty container
            if ind.IsNil() {
                ind.Set(reflect.MakeSlice(ind.Type(), 0, 0))
            }
        }
    }

    return cnt, nil
}
  • getOrderSQL方法,組裝排序sql
//====== orm/db_tables.go ======
func (t *dbTables) getOrderSQL(orders []string) (orderSQL string) {
    if len(orders) == 0 {
        return
    }

    Q := t.base.TableQuote()
    
    //根據orders切片長度,生成一個新的切片用于保存sql
    orderSqls := make([]string, 0, len(orders))
    for _, order := range orders {
        //設置排序方式,以'-'開頭為降序,否則為升序
        asc := "ASC"
        if order[0] == '-' {
            asc = "DESC"
            order = order[1:]
        }
        //根據ExprSep分隔符常量,將order字符串拆分為切片
        exprs := strings.Split(order, ExprSep)
        //解析模型字段
        index, _, fi, suc := t.parseExprs(t.mi, exprs)
        if !suc {
            panic(fmt.Errorf("unknown field/column name `%s`", strings.Join(exprs, ExprSep)))
        }
        //將解析結果格式化為字符串后追加到orderSqls切片
        orderSqls = append(orderSqls, fmt.Sprintf("%s.%s%s%s %s", index, Q, fi.column, Q, asc))
    }
    //將sql結果以逗號拼接為支付串后,組裝"ORDER BY "
    orderSQL = fmt.Sprintf("ORDER BY %s ", strings.Join(orderSqls, ", "))
    return
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容