微信小程序脚手架-后端项目封装04

WeChat Template

一套基于SpringBoot & Shiro 以及 Uni-app构建的微信小程序脚手架

开源地址: https://github.com/TyCoding/wechat-template 欢迎Star、Fork支持作者。

后端项目封装

今天我们讲解如何优雅的封装后端项目。

目录结构

└── src
    ├── main
    │   ├── java
    │   │   └── cn
    │   │       └── tycoding
    │   │           ├── Application.java
    │   │           ├── biz
    │   │           │   ├── controller  -- 控制层
    │   │           │   ├── entity      -- 实体类
    │   │           │   ├── mapper      -- Mybatis映射接口
    │   │           │   └── service     -- 业务层
    │   │           └── common          -- 公共层
    │   │               ├── auth        -- Shiro鉴权相关
    │   │               ├── config      -- 项目配置相关
    │   │               ├── constants   -- 项目公共常量
    │   │               ├── controller  -- 控制层公共方法提取
    │   │               ├── exception   -- 异常类
    │   │               ├── handler     -- 处理器类
    │   │               ├── properties  -- 框架配置参数
    │   │               └── utils       -- 工具类
    │   └── resources
    │       ├── application-dev.yml     -- 开发环境配置
    │       ├── application-prod.yml    -- 生产环境配置
    │       ├── application.yml         -- 项目基础配置
    │       ├── mapper                  -- Mybatis接口映射XML文件
    │       └── tycoding.properties     -- 项目自定义参数配置

Tips

以上是一个比较标准的项目目录结构,也可能一些人对此设计有所疑惑,下面我解释一下:

  1. cn.tycoding 代表作者的域名,也是强调项目开发者是谁
  2. biz Business,项目的控制层,包含了项目的主体业务代码
  3. common 通用层,包含了对项目基础环境的配置
  4. application-.yml 是按照SpringBoot默认的配置文件读取方式命名,dev代表开发环境,prod代表生产环境

Spring集成Mybatis

  1. pom.xml中添加mybatis依赖
<!-- Mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis.version}</version>
</dependency>
  1. 编写测试接口
/**
 * @author tycoding
 * @date 2020/6/9
 */
public interface UserMapper extends BaseMapper<SysUser> {
}
  1. 编写测试接口映射
<mapper namespace="cn.tycoding.biz.mapper.UserMapper">
</mapper>

Tips

以上就是简单在SpringBoot中集成Mybatis框架。你需要注意几点:

  1. 添加的依赖是:mybatis-sspring-boot-starter,好处就是SpringBoot已经自动对该框架做了基础配置
  2. 按照Mybatis的规定,仅需要提供一个interface继承BaseMapper即可,BaseMapper中提供了很多操控SQL的方法;其次如果需要使用XML自定义SQL,默认需要提供一个XML文件,他应当和上述接口的名称相同,目录名称也相同,并且XML中namespace必须指定这个interface的相对路径。按照SpringBoot的规定,XML文件必须写在resources目录下。

集成Mybatis-Plus

Mybatis框架本身提供了很多操控SQL的方法,但是对于分页、条件查询等可能并不能满足我们的需求,所以我们需要再集成Mybatis-plus

  1. pom.xml中添加mybatis-plus依赖:
 <!-- Mybatis Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>
  1. 编写Mybatis-plus配置类:
/**
 * @author tycoding
 * @date 2020/6/9
 */
@Configuration
public class MybatisPlusConfig {

    /**
     * Mybatis-Plus 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}
  1. application.yml中添加自定义配置,比如扫描哪里的xml文件等
mybatis-plus:
  type-aliases-package: cn.tycoding.biz.entity
  mapper-locations: classpath:mapper/*.xml
  configuration:
    jdbc-type-for-null: null
  global-config:
    banner: false
  1. 在Application.java中添加@MapperScan指定Mybatis接口类在哪里,避免Mybatis-plus找不到接口类的情况
/**
 * @author tycoding
 * @date 2020/6/9
 */
@SpringBootApplication
@MapperScan("cn.tycoding.biz.mapper")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 在Entity实体类中,根据Mybatis-plus的规定,如果Entity的名称与数据库中字段名称不同需要手动配置(遵循驼峰命名):
@Data
@Accessors(chain = true)
@TableName(value = "tb_user")
public class SysUser implements Serializable {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;
    private String password;
    private String avatar;
}

Spring集成Shiro

  1. pom.xml中添加shiro依赖:
<!-- Shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro-spring-version}</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>${shiro-ehcache.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 由于前面的包里面不包含这个,所以要单独引入, 如果不引入, shiro的权限注解不起作用 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring-aspects.version}</version>
</dependency>

为了保证Shiro注解能正常使用,最好同时引入aspectsaop的依赖。

  1. Shiro Config
/**
 * @author tycoding
 * @date 2020/6/9
 */
@Configuration
public class ShiroConfig {

    @Autowired
    private AppProperties properties;

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroProperties shiro = properties.getShiro();
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        filter.setSecurityManager(securityManager);
        filter.setLoginUrl(shiro.getLoginUrl());
        filter.setSuccessUrl(shiro.getSuccessUrl());

        Map<String, String> filterChain = new LinkedHashMap<>();
        String[] urls = shiro.getAnonUrl().split(",");
        for (String url : urls) {
            filterChain.put(url, "anon");
        }
        filterChain.put("/**", "user");
//        filterChain.put("/**", "anon");
        filter.setFilterChainDefinitionMap(filterChain);
        return filter;
    }

    @Bean
    public SecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(cacheManager());
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    @Bean
    public SimpleCookie rememberMeCookie() {
        ShiroProperties shiro = properties.getShiro();
        // 需要和前端<input name="remember">中的name对应
        SimpleCookie simpleCookie = new SimpleCookie("remember");
        simpleCookie.setMaxAge(shiro.getCookieTimeout());
        return simpleCookie;
    }

    @Bean
    public CookieRememberMeManager rememberMeManager() {
        ShiroProperties shiro = properties.getShiro();
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        cookieRememberMeManager.setCipherKey(Base64.decode(shiro.getCipherKey()));
        return cookieRememberMeManager;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public CacheManager cacheManager() {
        return new EhCacheManager();
    }

    @Bean
    public SessionDAO sessionDAO() {
        return new EnterpriseCacheSessionDAO();
    }

    @Bean
    public AuthSessionManager sessionManager() {
        // 自定义SessionManager,校验请求头中的Token信息
        AuthSessionManager sessionManager = new AuthSessionManager();
        Collection<SessionListener> listeners = new ArrayList<>();
        listeners.add(new ShiroSessionListener());
        // 设置 session超时时间
        sessionManager.setGlobalSessionTimeout(properties.getShiro().getSessionTimeout() * 1000L);
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionDAO(sessionDAO());
        return sessionManager;
    }

}
  1. 自定义Session Manager,用于在前后端分离项目中判断请求头是否包含Token
/**
 * @author tycoding
 * @date 2020/6/9
 */
public class AuthSessionManager extends DefaultWebSessionManager {

    public AuthSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // 获取请求头Header中的Token,登录时定义了Token = SessionID
        String token = WebUtils.toHttp(request).getHeader(CommonConstant.AUTHORIZATION);
        if (!StringUtils.isEmpty(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 否则按默认从Cookie中获取JSESSIONID
            return super.getSessionId(request, response);
        }
    }
}
  1. 自定义Realm
/**
 * @author tycoding
 * @date 2020/6/9
 */
@Component
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 身份校验
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        if (username == null) {
            throw new AuthenticationException(CommonEnum.TOKEN_ERROR.getMsg());
        }
        String password = new String((char[]) authenticationToken.getCredentials());
        SysUser sysUser = userService.findByName(username);

        if (sysUser == null || !sysUser.getPassword().equals(password)) {
            throw new IncorrectCredentialsException(CommonEnum.LOGIN_ERROR.getMsg());
        }

        return new SimpleAuthenticationInfo(
                sysUser,
                password,
                getName()
        );
    }

    /**
     * 权限校验
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}
  1. Shiro 密码加密配置
@Component
public class MD5Util {

    // 算法名称
    private static final String ALGORITH_NAME = "MD5";
    // 迭代次数
    private static final int HASH_ITERATIONS = 2;

    //加密算法
    public static String encryptPassword(String username, String password) {
        if (username == null || password == null) {
            return null;
        }
        return new SimpleHash(
                ALGORITH_NAME,
                StringUtils.lowerCase(StringUtils.lowerCase(password)),
                ByteSource.Util.bytes(StringUtils.lowerCase(username)),
                HASH_ITERATIONS).toHex();
    }
}

一般我们选择MD5的加密方式,而MD5只是一种哈希算法,每个字符串都有唯一的哈希值,也就是若单纯的对密码做MD5加密,他的加密值也和密码一样是恒定的,若保证安全,可以再加salt保证即使知道了加密方式和加密后的密码也得不到原密码。这个salt可以是随机值(那么密码就不会是恒定的了,每次做MD5加密密码都有改变),也可以是固定的,比如这里我们将username作为salt。

如何使用这个工具类(或者说如何加密密码)?这里以添加User的功能为例:

@Override
@Transactional
public void update(SysUser sysUser) {
    if (sysUser.getPassword() != null && sysUser.getUsername() != null) {
        // 加密
        String encrypt_password = MD5Util.encryptPassword(sysUser.getUsername(), sysUser.getPassword());
        sysUser.setPassword(encrypt_password);
    } else {
        sysUser.setPassword(null);
    }
    userMapper.updateById(sysUser);
}

可以看到,前端传来的是明文密码,而后端需要对明文面貌做加密处理得到encrypt password,然后将这个加密的密码保存到数据库中。当用户再登录时,Shiro会自动根据配置的加密算法和salt逆向得到加密的密码并与数据库中的密码比对。

Login

    /**
     * 登录接口
     *
     * @param username
     * @param password
     * @return
     */
@GetMapping("/login")
public R login(@RequestParam(value = "username", required = false) String username,
               @RequestParam(value = "password", required = false) String password) {
    if (username == null || password == null) {
        throw new GlobalException(CommonEnum.LOGIN_ERROR.getMsg());
    }
    Subject subject = getSubject();
    String encrypt_password = MD5Util.encryptPassword(username, password);
    UsernamePasswordToken token = new UsernamePasswordToken(username, encrypt_password);
    try {
        subject.login(token);

        Map<String, Object> map = new HashMap<>();
        map.put("token", subject.getSession().getId());
        map.put("user", this.getCurrentUser());
        return new R<>(map);
    } catch (Exception e) {
        e.printStackTrace();
        return new R<>(e);
    }
}

前端传来用户名和密码,后端将明文密码加密,再将用户名和加密后的密码传入UsernamePasswordToken对象中,调用subject.login(token)登录,Shiro将自动把加密后的密码与数据库中的密码相比对,如果相同则登录成功。登录成功后返回一个Map,包含了token值(这里以Session ID作为Token值)。

Logout

    /**
     * 注销接口
     *
     * @return
     */
    @DeleteMapping(value = "/logout")
    public R logout() {
        Subject subject = getSubject();
        subject.logout();
        return new R();
    }

注销接口仅需要调用subject.logout()即可清空Shiro中存储的Session等数据。

Others

全局数据返回格式封装

首先你要明白,前后端分离的实质是后端仅作为Restful返回数据,而这些数据通常是序列化后的JSON格式吗,因此不要脱离JSON格式去传输数据

一些人很不理解为什么要对数据返回格式进行封装,我直接返回数据不行吗?

栗子:

Request:

get http://localhost:8080/user/info

Response:

{
    "username": "123",
    "password": "123"
}

有毛病吗?没毛病,你确实可以这样写。但是在前后端分离项目中,你返回这数据想让前端开发者怎么处理?首先这数据代表什么?其次返回的状态是什么?最后如果接口出现异常你返回什么?

针对以上,接口返回的数据中应当包含了:

  1. code:响应状态码
  2. data:存储响应数据
  3. msg:异常返回信息

因此,我们可以猜到,接口响应格式应该是这样的:

{
    "code": 200,
    "data": {
        "username": "123",
        "password": "123"
    },
    "msg": "success"
}

对此,需要封装一个包含响应格式的类:

/**
 * @author tycoding
 * @date 2020/6/9
 */
@Builder
@ToString
@AllArgsConstructor
public class R<T> implements Serializable {

    @Getter
    @Setter
    private int code = CommonConstant.SUCCESS;

    @Getter
    @Setter
    private Object msg = "success";

    @Getter
    @Setter
    private T data;

    public R() {
        super();
    }

    public R(T data) {
        super();
        this.data = data;
    }

    public R(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public R(T data, String msg) {
        super();
        this.data = data;
        this.msg = msg;
    }

    public R(CommonEnum enums) {
        super();
        this.code = enums.getCode();
        this.msg = enums.getMsg();
    }

    public R(Throwable e) {
        super();
        this.code = CommonConstant.ERROR;
        this.msg = e.getMessage();
    }

    public R(String message, Throwable e) {
        super();
        this.msg = message;
        this.code = CommonConstant.ERROR;
    }
}

此类已经近乎包含了所有返回格式的情况。如何使用?

@GetMapping("/{id}")
public R findById(@PathVariable Long id) {
    return new R<>(userService.getById(id));
}

直接return new R<>()即可。

全局异常封装

整个项目运行过程中会产生很多异常,而Spring等框架默认是返回一个error page,这中方式及其不友好(特别是在单体项目中),因此我们需要对全局异常进行拦截和封装。

首先写一个自定义异常类:

public class GlobalException extends RuntimeException {

    @Getter
    @Setter
    private String msg;

    public GlobalException(String message) {
        super(message);
        this.msg = message;
    }
}

再写异常类对应的Handle:

/**
 * 全局异常处理器
 *
 * @author tycoding
 * @date 2020/6/9
 */
@Slf4j
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public R exception(Exception e) {
        log.error("内部错误, {}", e.getMessage());
        e.printStackTrace();
        return new R(e);
    }

    @ExceptionHandler(value = GlobalException.class)
    public R globalExceptionHandle(GlobalException e) {
        log.error("全局异常, {}", e.getMessage());
        e.printStackTrace();
        return new R<>(CommonConstant.ERROR, e.getMsg());
    }

    @ExceptionHandler(value = UnauthorizedException.class)
    public R handleUnauthorizedException(UnauthorizedException e) {
        log.error("UnauthorizedException, {}", e.getMessage());
        return new R(HttpStatus.FORBIDDEN, e.getMessage());
    }

    @ExceptionHandler(value = AuthenticationException.class)
    public R handleAuthenticationException(AuthenticationException e) {
        log.error("AuthenticationException, {}", e.getMessage());
        return new R(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }

    @ExceptionHandler(value = AuthorizationException.class)
    public R handleAuthorizationException(AuthorizationException e) {
        log.error("AuthorizationException, {}", e.getMessage());
        return new R<>(HttpStatus.UNAUTHORIZED, e.getMessage());
    }
}
  • @RestControllerAdvice 简单来说他可以应用在所有包含@RequestMapping注解的类中,可以做一些自定义处理
  • @Order(value = Ordered.HIGHEST_PRECEDENCE) 表示此过滤器在所有过滤器中优先级最高
  • @ExceptionHandler 捕捉@RequestMapping类中产生的异常,自定义返回值信息

综上也就是说,此Handle仅仅对于项目中Controller类有效,对于未包含@RequestMapping的类,此Handle是无法拦截其抛出的异常的。但是我们仍可以抛出GlobalException,然后在controller中进行try catch处理。

例如在UserService的新增方法中,用户名可能已存在而产生异常,需要手动throw GlobalException,而此GlobalExceptionGlobalExceptionHandler是无法进行处理的(也就是说即使定义了@ExceptionHandler(value = Exception.class)也无法拦截到此异常),那么就需要手动在Controller层try catch处理该异常,实现自定义异常返回信息:

UserServiceImpl

@Override
@Transactional
public void add(SysUser sysUser) {
    SysUser user = this.findByName(sysUser.getUsername());
    if (user != null) {
        throw new GlobalException("该用户名已存在");
    }
    // 加密
    String encrypt_password = MD5Util.encryptPassword(sysUser.getUsername(), sysUser.getPassword());
    sysUser.setPassword(encrypt_password);
    userMapper.insert(sysUser);
}

UserController

@PostMapping
public R save(@RequestBody SysUser sysUser) {
    try {
        userService.add(sysUser);
        return new R();
    } catch (Exception e) {
        throw new GlobalException(e.getMessage());
    }
}

BaseController封装

之前大家一直在写Controller,项目中也用到了Mybatis-plus和Shiro框架,那么必不可少项目中会经常使用两个功能:

  1. 数据分页。(前端使用UI分页组件,后端需要按照分页参数查询指定页码数据)
  2. Shiro中sessionsubject对象的经常调用

因此,可以预先封装一个BaseController,其中包含常用的方法,其他的Controller继承这个BseController就可以直接调用方法了,提高了代码的复用性:

/**
 * Controller公共层方法提取
 *
 * @author tycoding
 * @date 2020/6/9
 */
public class BaseController {

    protected static Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    protected SysUser getCurrentUser() {
        return (SysUser) getSubject().getPrincipal();
    }

    protected Session getSession() {
        return getSubject().getSession();
    }

    protected Session getSession(Boolean flag) {
        return getSubject().getSession(flag);
    }

    protected void login(AuthenticationToken token) {
        getSubject().login(token);
    }

    public Map<String, Object> getData(IPage<?> page) {
        Map<String, Object> data = new HashMap<>();
        data.put("rows", page.getRecords());
        data.put("total", page.getTotal());
        return data;
    }

    public Map<String, Object> getToken() {
        Map<String, Object> map = new HashMap<>();
        map.put("token", getSession().getId());
        return map;
    }
}

例如在UserController中需要些分页接口:

@PostMapping("/list")
public R findByPage(@RequestBody SysUser sysUser, QueryPage queryPage) {
    return new R<>(super.getData(userService.list(sysUser, queryPage)));
}

直接调用super.getData就可以自定将UserService返回的数据做分页的封装处理(主要是Map参数格式)

全局分页数据格式封装

在上面已经提到了项目中肯定经常用到数据分页,那么必须定义一个包含分页数据格式的类:

/**
 * @author tycoding
 * @date 2020/6/9
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QueryPage implements Serializable {

    /**
     * 当前页
     */
    private int page;

    /**
     * 每页的记录数
     */
    private int limit;
}

项目公共常量

很多时候,后端需要定义一个指定名称的属性,而这个属性又不能写死,得方便修改,那么我们一般定义一个公共的常量类存储项目中用到常量:

/**
 * 项目公共常量
 *
 * @author tycoding
 * @date 2020/6/9
 */
public interface CommonConstant {

    /**
     * 前端Token Key
     */
    String AUTHORIZATION = "Authorization";

}

而定义interface的原因就是在其他类中可以方便的调用CommonConstant.AUTHORIZATION快速获取某个属性值。

全局定义配置文件类

例如Shiro、Swagger等框架在使用时需要自定义配置,而这些配置是不能直接在application.yml中定义的,于是你可以写在resources/目录下,一般以.properties结尾命名,Spring会自动扫描到这个配置文件。

但Spring自动扫描了,并不会自动给你配置到某个类中,需要手动将其注入到Spring Environment,然后再获取到并定义到某个类中:

/**
 * @author tycoding
 * @date 2020/6/9
 */
@Data
@SpringBootConfiguration
@PropertySource(value = {"classpath:tycoding.properties"})
@ConfigurationProperties(prefix = "tycoding")
public class AppProperties {

    private ShiroProperties shiro = new ShiroProperties();
}
  • @SpringBootConfiguration 指定这是SpringBoot项目的配置文件类
  • @PropertySource 将配置文件加载到Spring Environment中,value指定了配置文件的配置,classpath也就是resources目录的相对路径
  • @ConfigurationProperties 扫描包含指定prefix前缀的配置

这种方式最大的好处就是可以根据此类中定义的对应名称自动在配置文件中查找指定的配置并注入到配置类中。

例如上面写的AppProperties类中声明了ShiroProperties shiro对象,那么Spring自动扫描tycoding.properties文件中以tycoding.shiro开头的配置参数,这些配置参数将按照驼峰命名规则自动装配到ShiroProperties对象属性中。

首先看下tycoding.properties的配置:

tycoding.shiro.session_timeout=3600
tycoding.shiro.cookie_timeout=86400
tycoding.shiro.anon_url=/login,/logout
tycoding.shiro.login_url=/login
tycoding.shiro.success_url=/index
tycoding.shiro.logout_url=/logout
tycoding.shiro.cipher_key=tycoding

再看下ShiroProperties类的属性:

@Data
public class ShiroProperties {

    private long sessionTimeout;
    private int cookieTimeout;
    private String anonUrl;
    private String loginUrl;
    private String successUrl;
    private String logoutUrl;
    private String cipherKey;
}

那么如果要使用tycoding.properties的配置参数,直接在类中初始化ShiroProperties shiro对象,然后调用shiro.xx即可获取到参数配置。

交流

QQGroup:671017003

WeChatGroup: 关注公众号查看

联系

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