retrofit callback模式的封装

几类请求的实现和通用性的封装

String和json类型:

使用的接口

public static ConfigInfo getString(String url, Map map, MyNetListener listener)

public static ConfigInfo postString( String url,  Map map,  MyNetListener listener)

public static ConfigInfo postStandardJson( String url,  Map map, Class clazz, MyNetListener listener)

public static ConfigInfo getStandardJson( String url,  Map map, Class clazz, MyNetListener listener)

public static ConfigInfo postCommonJson( String url,  Map map, Class clazz, MyNetListener listener)

public static ConfigInfo getCommonJson( String url,  Map map, Class clazz, MyNetListener listener)

retrofit 定义通用的接口方法:

/**
 * 注意:
 * 1.retrofit默认转换string成json obj,如果不需要gson转换,那么就指定泛型为ResponseBody,
 *  只能是ResponseBody,子类都不行,同理,下载上传时,也必须指定泛型为ResponseBody
 * 2. map不能为null,否则该请求不会执行,但可以size为空
 * 3使用@url,而不是@Path注解,后者放到方法体上,会强制先urlencode,然后与baseurl拼接,请求无法成功
 * @param url
 * @param maps
 * @return
 */
@FormUrlEncoded
@POST()
Call<ResponseBody> executePost(@Url String url, @FieldMap Map<String, String> maps);

@GET()
Call<ResponseBody> executGet(@Url String url, @QueryMap Map<String, String> maps);

踩过的坑:

获取纯String类型时,不能指定为executGet的泛型为String,而必须是原生的ResponseBody.

此时无需设置json转换器,而是拿到ResponseBody的data字段,调用其string()方法,转换成String,然后自己用json转换器解析.

//而不能是:
 @GET()
Call<String> executGet(@Url String url, @QueryMap Map<String, String> maps);

//如果这样指定,意思是,使用retrofit内部的json转换器,将response里的数据转换成一个实体类xxx,比如UserBean之类的,而String类明显不是一个有效的实体类,自然转换失败.

retrofit不支持二次泛型(以下形式都不支持)

 /**
 * 标准格式的json(data,msg,code)解析:泛型嵌套 无法实现: retrofit不支持二次泛型
    https://github.com/square/retrofit/issues/2012
    报的错误
   Method return type must not include a type variable or wildcard: retrofit2.Call<T>
 *
 * JakeWharton的回复:
 You cannot. Type information needs to be fully known at runtime in order for deserialization to work.
 *     
 
 * */
 
@FormUrlEncoded
@POST()
<T>  Call<T> postCommonJson(@Url String url, @FieldMap Map<String, String> maps);

@GET()
<T>  Call<T> getCommonJson(@Url String url, @QueryMap Map<String, String> maps);
 
 
@FormUrlEncoded
@POST()
<T>  Call<BaseNetBean<T>> postStandradJson(@Url String url, @FieldMap Map<String, String> maps);

@GET()
<T>  Call<BaseNetBean<T>> getStandradJson(@Url String url, @QueryMap Map<String, String> maps);

泛型擦除

想通过方法上指定的泛型直接拿来解析json,但是报异常:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to 

https://www.zhihu.com/question/27216298
代码如下: 其中E是方法上指定的泛型

 Gson gson = new Gson();
 Type objectType = new TypeToken<E>() {}.getType();
 final E bean = gson.fromJson(string,objectType);

无奈,只能通过传入Class对象来进行老老实实的解析.

文件下载

使用的接口

public static ConfigInfo download(String url, String savedpath, MyNetListener callback)

retrofit接口:

@Streaming //流式下载,不加这个注解的话,会整个文件字节数组全部加载进内存,可能导致oom
@GET
Call<ResponseBody> download(@Url String fileUrl);

写文件

流式下载,所以回调是在子线程中,要自己实现写文件的代码,并在写完后切回主线程

 public void onResponse(Call<ResponseBody> call, final Response<ResponseBody> response) {
 ...
   //开子线程将文件写到指定路径中
            SimpleTask<Boolean> simple = new SimpleTask<Boolean>() {

                @Override
                protected Boolean doInBackground() {
                    return writeResponseBodyToDisk(response.body(),configInfo.filePath);//写文件
                }

                @Override
                protected void onPostExecute(Boolean result) {
                    if (result){
                        configInfo.listener.onSuccess(configInfo.filePath,configInfo.filePath);
                    }else {
                        configInfo.listener.onError("文件下载失败");
                    }
                }
            };
            simple.execute();

进度回调的两种实现方法

拦截器实现:

  .addInterceptor(new ProgressInterceptor())//下载时更新进度
  
  //拦截器里:
  public Response intercept(Interceptor.Chain chain) throws IOException{
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),chain.request().url().toString())).build();
}

//ProgressResponseBody: 
// 这里使用eventbus来按一定的时间间隔传递进度,避免可能过度频繁更新ui.
//使用url来确定进度event的所属,避免多个同时下载时进度错乱.
public class ProgressResponseBody extends ResponseBody {

    private final ResponseBody responseBody;

    private BufferedSource bufferedSource;
    private String url;

    public ProgressResponseBody(ResponseBody responseBody,String url) {
        this.responseBody = responseBody;
        this.url = url;

    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }


    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }

    long timePre = 0;
    long timeNow;

    private Source source(final Source source) {
        return new ForwardingSource(source) {
            long totalBytesRead = 0L;

            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                timeNow = System.currentTimeMillis();

                if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次状态
                    timePre = timeNow;
                    EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(),
                            totalBytesRead == responseBody.contentLength(),url));
                }
                return bytesRead;
            }
        };
    }
}

//构建下载请求时注册eventbus:
  configInfo.listener.registEventBus();
  
 //预先设置好url
configInfo.listener.url = url;
  
//在MyNetListener里接收进度:
 @Subscribe(threadMode = ThreadMode.MAIN)
public void  onMessage(ProgressEvent event){
    if (event.url.equals(url)){
        onProgressChange(event.totalLength,event.totalBytesRead);
        if (event.done){
            unRegistEventBus();
            onFinish();
        }
    }
}

写文件时回调进度

上面这种拦截模式有些繁琐,不如在写文件时控制.

            byte[] fileReader = new byte[4096];
            long fileSize = body.contentLength();
            long fileSizeDownloaded = 0;

            inputStream = body.byteStream();
            outputStream = new FileOutputStream(futureStudioIconFile);
            while (true) {
                int read = inputStream.read(fileReader);
                if (read == -1) {
                    break;
                }
                outputStream.write(fileReader, 0, read);
                fileSizeDownloaded += read;
                Log.d("io", "file download: " + fileSizeDownloaded + " of " + fileSize);//// TODO: 2016/9/21  这里也可以实现进度监听
            }
            outputStream.flush();

文件上传

1474443145230_2.png

使用的接口

 public static ConfigInfo upLoad(String url, Map<String,String> params,Map<String,String> files, MyNetListener callback)

两种实现方式:

/**
 * 无法实现进度回调
 * @param url
 * @param multipartBody
 * @return
 */
@POST()
Call<ResponseBody> upload(@Url String url,@Body MultipartBody multipartBody);


/**
 * 可以有进度回调
 * @param url
 * @param options
 * @param externalFileParameters
 * @return
 */
@POST()
@Multipart
Call<ResponseBody> uploadWithProgress(@Url String url,@QueryMap Map<String, String> options,@PartMap Map<String, RequestBody> externalFileParameters) ;

第二种的参数拼接

使用的是UploadFileRequestBody,用于进度回调

 @Override
protected  Call newUploadRequest(final ConfigInfo configInfo) {
    if (serviceUpload == null){
        initUpload();
    }
    configInfo.listener.registEventBus();
    Map<String, RequestBody> requestBodyMap = new HashMap<>();
    if (configInfo.files != null && configInfo.files.size() >0){
        Map<String,String> files = configInfo.files;
        int count = files.size();
        if (count>0){
            Set<Map.Entry<String,String>> set = files.entrySet();
            for (Map.Entry<String,String> entry : set){
                String key = entry.getKey();
                String value = entry.getValue();
                File file = new File(value);
                String type = Tool.getMimeType(file);
                Log.e("type","mimetype:"+type);
                UploadFileRequestBody fileRequestBody = new UploadFileRequestBody(file, type,configInfo.url);
                requestBodyMap.put(key+"\"; filename=\"" + file.getName(), fileRequestBody);//关键之处,key放在这里
            }
        }
    }
    
    Call<ResponseBody> call = service.uploadWithProgress(configInfo.url,configInfo.params,requestBodyMap);
    ...

上传进度回调的实现

UploadFileRequestBody 如下.而eventbus相关设置同下载.

public class UploadFileRequestBody extends RequestBody {

    private RequestBody mRequestBody;
    private BufferedSink bufferedSink;
    private String url;


    public UploadFileRequestBody(File file,String mimeType,String url) {
       // this.mRequestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
        this.mRequestBody = RequestBody.create(MediaType.parse(mimeType), file);
        this.url = url;
    }
    
    @Override
    public MediaType contentType() {
        return mRequestBody.contentType();
    }

    //返回了本RequestBody的长度,也就是上传的totalLength
    @Override
    public long contentLength() throws IOException {
        return mRequestBody.contentLength();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        if (bufferedSink == null) {
            //包装
            bufferedSink = Okio.buffer(sink(sink));
        }
        //写入
        mRequestBody.writeTo(bufferedSink);
        //必须调用flush,否则最后一部分数据可能不会被写入
        bufferedSink.flush();
    }

    long oldTime = 0L;

    private Sink sink(Sink sink) {
        return new ForwardingSink(sink) {
            //当前写入字节数
            long bytesWritten = 0L;
            //总字节长度,避免多次调用contentLength()方法
            long contentLength = 0L;
            @Override
            public void write(Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
                if (contentLength == 0) {
                    //获得contentLength的值,后续不再调用
                    contentLength = contentLength();
                }
                //增加当前写入的字节数
                bytesWritten += byteCount;
                long currentTime = System.currentTimeMillis();
                if (currentTime - oldTime > NetDefaultConfig.PROGRESS_INTERMEDIATE || bytesWritten == contentLength){//每300ms更新一次进度
                    oldTime = currentTime;
                    EventBus.getDefault().post(new ProgressEvent(contentLength,bytesWritten,bytesWritten == contentLength,url));
                }
            }
        };
    }
}

useragent设置:

默认是okhttp/3.3.0,将其改成volley模式下的字样,信息丰富些:
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; SM-N7508V Build/JLS36C)

其构成依次是: 虚拟机名/虚拟机版本号(系统内核名;内核版本号;安卓系统版本号;手机型号 版本号
不用自己拼接,直接调用代码就可以.在初始化时拿到,设置成一个常量.

String userAgent = System.getProperty("http.agent");

public class UseragentInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
       Request request =  chain.request();
      request =   request.newBuilder().addHeader("User-Agent", NetDefaultConfig.USER_AGENT).build();
        return chain.proceed(request);
    }
}

完全的客户端缓存控制

屏蔽原网络框架本身的缓存功能:

主要是因为,okhttp只缓存get请求,不缓存post请求.但实际工作中有时需要缓存post请求的数据.

屏蔽的思路: 请求头cacheControl指定为no-cache,然后用拦截器修改响应头,移除expeirs,pragma之类的字段并把cacheControl改为no-cache.

public class NoCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
       
        Request request = chain.request();
        request = request.newBuilder().header("Cache-Control","no-cache").build();
        Response originalResponse = chain.proceed(request);
        originalResponse = originalResponse.newBuilder().header("Cache-Control","no-cache").build();
        return originalResponse;
        }
 }

配置单个请求的缓存策略(默认是无缓存)

只针对String和json的请求结果的缓存,缓存的形式是String,带有有效期.

ConfigInfo:
 /**
 * 只支持String和json类型的请求,不支持文件下载的缓存.
 * @param shouldReadCache 是否先去读缓存
 * @param shouldCacheResponse 是否缓存response  内部已做判断,只会缓存状态是成功的那些请求
 * @param cacheTimeInSeconds 缓存的时间,单位是秒
 * @return
 */
public ConfigInfo<T> setCacheControl(boolean shouldReadCache,boolean shouldCacheResponse,long cacheTimeInSeconds){
    this.shouldReadCache = shouldReadCache;
    this.shouldCacheResponse = shouldCacheResponse;
    this.cacheTime = cacheTimeInSeconds;
    return this;

}

缓存实现的代码

构建请求对象前,先读缓存:

BaseNet:

 public <E> ConfigInfo<E> start(ConfigInfo<E> configInfo) {

    String url = Tool.appendUrl(configInfo.url, isAppendUrl());
    configInfo.url = url;
    configInfo.listener.url = url;

    if (configInfo.isAppendToken){

        Tool.addToken(configInfo.params);
    }

   // configInfo.client = this;

    if (getCache(configInfo)){//读缓存,异步操作
        return configInfo;
    }

    T request = generateNewRequest(configInfo);
}

读缓存:

 private <E> boolean getCache(final ConfigInfo<E> configInfo) {
    switch (configInfo.type){
        case ConfigInfo.TYPE_STRING:
        case ConfigInfo.TYPE_JSON:
        case ConfigInfo.TYPE_JSON_FORMATTED:{
            //拿缓存
            if (configInfo.shouldReadCache){
                final long time = System.currentTimeMillis();
                SimpleTask<String> simple = new SimpleTask<String>() {
                    @Override
                    protected String doInBackground() {
                        return ACache.get(MyNetUtil.context).getAsString(CommonHelper.getCacheKey(configInfo));
                    }

                    @Override
                    protected void onPostExecute(String result) {
                        if (TextUtils.isEmpty(result)){
                            configInfo.shouldReadCache = false;
                            start(configInfo);//没有缓存就去访问网络
                        }else {//解析从缓存中拿到的结果
                            configInfo.isFromCache = true;
                            Tool.parseStringByType(time,result,configInfo);
                        }

                    }
                };
                simple.execute();
            }else {
                return false;
            }
        }
        case ConfigInfo.TYPE_DOWNLOAD:
        case ConfigInfo.TYPE_UPLOAD_WITH_PROGRESS:
        case ConfigInfo.TYPE_UPLOAD_NONE_PROGRESS:
            return false;
        default:return false;
    }
}

请求结果的缓存

//解析从缓存中拿到的结果,如果不是从缓存中拿到的,才缓存.
//对于标准json,还有一个条件:只有code是成功的code时,才缓存(调用下面方法).

private static void cacheResponse(final String string, final ConfigInfo configInfo) {
    if (configInfo.shouldCacheResponse && !configInfo.isFromCache && configInfo.cacheTime >0){
        SimpleTask<Void> simple = new SimpleTask<Void>() {

            @Override
            protected Void doInBackground() {
                ACache.get(MyNetUtil.context).put(getCacheKey(configInfo),string, (int) (configInfo.cacheTime));
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
            }
        };
        simple.execute();
    }
}

标准json请求的设置和解析

请求的配置

默认字段和code码

ConfigInfo:
public  static  String KEY_DATA = "data";
public static  String KEY_CODE = "code";
public static  String KEY_MSG = "msg";

BaseNetBean:
public static final int CODE_NONE = -1;
public static  int CODE_SUCCESS = 0;
public static  int CODE_UNLOGIN = 2;
public static  int CODE_UN_FOUND = 3;

全局配置

MyNetApi:

/**
 * 指定标准格式json的三个字段.比如聚合api的三个字段分别是error_code(但有的又是resultcode),reason,result,error_code
 * @param data
 * @param code
 * @param msg
 * @param codeSuccess
 * @param codeUnlogin
 * @param codeUnfound
 */
public static void setStandardJsonKey(String data,String code,String msg,int codeSuccess,int codeUnlogin,int codeUnfound){
    NetDefaultConfig.KEY_DATA = data;
    NetDefaultConfig.KEY_CODE = code;
    NetDefaultConfig.KEY_MSG = msg;
    BaseNetBean.CODE_SUCCESS = codeSuccess;
    BaseNetBean.CODE_UNLOGIN = codeUnlogin;
    BaseNetBean.CODE_UN_FOUND = codeUnfound;
}

单个请求的配置

ConfigInfo:

 public ConfigInfo<T> setStandardJsonKey(String keyData,String keyCode,String keyMsg){
    this.key_data = keyData;
    this.key_code = keyCode;
    this.key_msg = keyMsg;
    return this;
}

 public ConfigInfo<T> setStandardJsonKeyCode(String keyCode){
    this.key_code = keyCode;
    return this;

}

 public ConfigInfo<T> setCustomCodeValue(int code_success,int code_unlogin,int code_unFound){
    this.code_success = code_success;
    this.code_unlogin = code_unlogin;
    this.code_unFound = code_unFound;
    isCustomCodeSet = true;
    return this;
}

以上配置的json解析代码:

//通过key去拿json里对应的值:
        String key_data = TextUtils.isEmpty(configInfo.key_data) ? NetDefaultConfig.KEY_DATA : configInfo.key_data;
        String key_code = TextUtils.isEmpty(configInfo.key_code) ? NetDefaultConfig.KEY_CODE : configInfo.key_code;
        String key_msg = TextUtils.isEmpty(configInfo.key_msg) ? NetDefaultConfig.KEY_MSG : configInfo.key_msg;

        final String dataStr = object.optString(key_data);
        final int code = object.optInt(key_code);
        final String msg = object.optString(key_msg);
        

//根据code来执行回调:
 private static <E> void parseStandardJsonObj(String response,String data,int code,String msg, 
    final ConfigInfo<E> configInfo){

    int codeSuccess = configInfo.isCustomCodeSet ? configInfo.code_success : BaseNetBean.CODE_SUCCESS;
    int codeUnFound = configInfo.isCustomCodeSet ? configInfo.code_unFound : BaseNetBean.CODE_UN_FOUND;
    int codeUnlogin = configInfo.isCustomCodeSet ? configInfo.code_unlogin : BaseNetBean.CODE_UNLOGIN;

    if (code == codeSuccess){
        if (isJsonEmpty(data)){
            configInfo.listener.onEmpty();
        }else {
            try{
                if (data.startsWith("{")){
                    E bean =  MyJson.parseObject(data,configInfo.clazz);
                    configInfo.listener.onSuccessObj(bean ,response,data,code,msg);
                    cacheResponse(response, configInfo);
                }else if (data.startsWith("[")){
                    List<E> beans =  MyJson.parseArray(data,configInfo.clazz);
                    configInfo.listener.onSuccessArr(beans,response,data,code,msg);
                    cacheResponse(response, configInfo);
                }else {//如果data的值是一个字符串,而不是标准json,那么直接返回
                    if (String.class.equals(configInfo.clazz) ){//此时,E也是String类型.如果有误,会抛出到下面catch里
                        configInfo.listener.onSuccess((E) data,data);
                    }else {
                         configInfo.listener.onError("不是标准的json数据");
                    }
                }

            }catch (Exception e){
                e.printStackTrace();
                configInfo.listener.onError(e.toString());
                return;
            }
        }
    }else if (code == codeUnFound){
        configInfo.listener.onUnFound();
    }else if (code == codeUnlogin){
        configInfo.client.autoLogin(new MyNetListener() {
            @Override
            public void onSuccess(Object response, String resonseStr) {
                configInfo.client.resend(configInfo);
            }

            @Override
            public void onError(String error) {
                super.onError(error);
                configInfo.listener.onUnlogin();
            }
        });
    }else {
        configInfo.listener.onCodeError("",msg,code);
    }

}

登录状态和自动登录

定义接口:

//逻辑
public interface ILoginManager {

    boolean isLogin();

    <T>  ConfigInfo<T> autoLogin();

    <T> ConfigInfo<T> autoLogin(MyNetListener<T> listener);
}


//用于对接
public  interface INet extends ILoginManager

传递给INet的子类BaseNet

public  abstract class BaseNet<T> implements INet {
//T: 请求类  call或者是Request

    private ILoginManager loginManager;

    public void setLoginManager(ILoginManager loginManager){
        this.loginManager = loginManager;
    }
    
     @Override
    public <E> ConfigInfo<E> autoLogin() {
        if (loginManager != null){
          return   loginManager.autoLogin();
        }
        return null;
    }

    @Override
    public <E> ConfigInfo<E> autoLogin(MyNetListener<E> myNetListener) {
        if (loginManager != null){
            return   loginManager.autoLogin(myNetListener);
        }
        return null;

    }

    @Override
    public boolean isLogin() {
        if (loginManager != null){
            return loginManager.isLogin();
        }
        return false;
    }
...
}

顶级api传入BaseNet和ILoginManager,包装一层:

使用时,BaseNet采用Retrofit.getInstance(),而ILoginManager则由用户自己实现.

public class MyNetApi {

    public static Context context;
    public static BaseNet adapter;

     public static void init(Context context,BaseNet adapter,ILoginManager loginManager){
        MyNetApi.context = context;
        MyNetApi.adapter = adapter;
        if (loginManager instanceof  BaseNet){
            throw  new RuntimeException("please implement ILoginManager independently");
            //避免可能的无限循环调用
        }
        MyNetApi.adapter.setLoginManager(loginManager);

}
    
    public static ConfigInfo autoLogin() {
        return  adapter.autoLogin();
    }

    public static ConfigInfo autoLogin(MyNetListener myNetListener) {
        return  adapter.autoLogin(myNetListener);
    }

    public static boolean isLogin(){
        return adapter.isLogin();
    }
    
    ...
}

请求最短回调时间的设置

主要针对如下情况:

发送网络请求之前弹出一个dialog来提示加载中,回调成功后dismiss并关掉整个activity.如果回调很快,可能会出现: dialog还没有弹出来,activity就关掉了,crash,日志为bad windowToken,is your activity running? 这时,需要指定最短回调时间,一般dialog弹出需要几百毫秒不等,限定1s或2s即可.

设置:

/**
 *
 * @param isForceMinTime 是否强制最短时间
 * @param minTime 自定义的最短时间.如果为小于0,则采用默认的1500ms
 * @return
 */
public ConfigInfo<T> setMinCallbackTime(boolean isForceMinTime,int minTime){
    this.isForceMinTime = isForceMinTime;
    this.minTime = minTime;
    return this;
}

实现:利用Timer

 /**
 *  
 * @param startTime 请求刚开始的时间
 * @param configInfo 
 * @param runnable 要执行的代码,通常是最终的网络回调
 * @param <T>
 */
public static <T> void parseInTime(long startTime, ConfigInfo<T> configInfo, final Runnable runnable) {
    long time2 = System.currentTimeMillis();
    long gap = time2 - startTime;

    if (configInfo.isForceMinTime ){
        long minGap = configInfo.minTime <= 0 ? NetDefaultConfig.TIME_MINI : configInfo.minTime;

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

推荐阅读更多精彩内容