iOS Swift H5 WKWebView交互麦克风录音完访问本地文件路径遇到的问题及解决方案

##更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!

需求分析:

最近项目需求需要麦克风录音权限,因为整体上的UI界面是前端wkwebview搭建的,实现功能逻辑是由iOS实现,没有用原生!然后就出现了需要麦克风录音机跟H5交互的功能模块!

查了资料都文章说iOS对h5交互麦克风录音不友好
现在具体工作流程步骤如下:

  1. 首先创建了一个wkwebview
//加载webview视图
    override func loadView() {
        let preference = WKPreferences()
        preference.minimumFontSize = 0
        preference.javaScriptEnabled = true
        preference.javaScriptCanOpenWindowsAutomatically = true
        preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
        debugPrint("这里已经进来了")
        
        // swift 提供给 h5 调用方法
        let userContentController = WKUserContentController()
        userContentController.add(self, name: "callAudio")  //调起iOS音频权限
        userContentController.add(self, name: "recorderStart")  //开始录音
        userContentController.add(self, name: "recorderStop")  //停止录音
        
        let conf = WKWebViewConfiguration()
        conf.userContentController = userContentController
        conf.preferences = preference
        
//        let conf = WKWebViewConfiguration();
//        conf.userContentController.add(self, name: "callAudio")  //调起iOS音频权限
//        conf.userContentController.add(self, name: "recorderStart")  //开始录音
//        conf.userContentController.add(self, name: "recorderStop")  //停止录音
        webView = WKWebView(frame: CGRect(x:0, y:0, width:SCREEN_WIDTH, height:SCREEN_HEIGHT), configuration: conf)
        webView.navigationDelegate = self;
        webView.scrollView.isScrollEnabled = false  //禁止webview滑动滚动
        if #available(iOS 11.0, *) {
            webView.scrollView.contentInsetAdjustmentBehavior = .never;
        }
        view = webView;
    }

其中:callAudio、recorderStart、recorderStop是iOS跟webview定义好协议接收的方法

  1. 重点:加载完成后接收H5调用的协议方法:
// 接受 h5 调用
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let name = message.value(forKey: "name") as? String, let body = message.value(forKey: "body") as? String  else { return }
        debugPrint("测试链接8888+:\(name)")
        if name == "callAudio" {
            SystemAuth.authMicrophone { result in
                if result{
                    self.webView.evaluateJavaScript("getPermission('\(result)')", completionHandler: nil)
                }else{
                    DispatchQueue.main.async {
                        let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
                        alertView.show()
                    }
                }
            }
        }
}

SystemAuth.authMicrophone 调用录音麦克风权限返回true跟false
self.webView.evaluateJavaScript("getPermission('(result)')", completionHandler: nil) iOS拦截到方法注入新方法getPermission()携带参数true或者false返回给H5接收

Swift开启iOS的录音权限包括其他照相机权限的代码文件

我整理好在下面的代码了

SystemAuth.Swift

//
//  SystemAuth.swift
//  Authorization
//
//  Created by 柯南 on 2020/9/4.
//  Copyright © 2020 LTM. All rights reserved.
//

import UIKit

/// 媒体资料库/Apple Music
import MediaPlayer
import Photos
import UserNotifications
import Contacts
/// Siri权限
import Intents
/// 语音转文字权限
import Speech
/// 日历、提醒事项
import EventKit
/// Face、TouchID
import LocalAuthentication
import HealthKit
import HomeKit
/// 运动与健身权限
import CoreMotion
/// 防止获取无效 计步器
private let cmPedometer = CMPedometer()

typealias AuthClouser = ((Bool)->())

/// 定义私有全局变量,解决在iOS 13 定位权限弹框自动消失的问题
private let locationAuthManager = CLLocationManager()

/**
 escaping 逃逸闭包的生命周期:
 
 1,闭包作为参数传递给函数;
 
 2,退出函数;
 
 3,闭包被调用,闭包生命周期结束
 即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放
 经常使用逃逸闭包的2个场景:
 异步调用: 如果需要调度队列中异步调用闭包,比如网络请求成功的回调和失败的回调,这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不确定,上边的例子。
 存储: 需要存储闭包作为属性,全局变量或其他类型做稍后使用,例子待补充
 */
public class SystemAuth {
    
//    /**
//     媒体资料库/Apple Music权限
//
//     - parameters: action 权限结果闭包
//     */
//    class func authMediaPlayerService(clouser :@escaping AuthClouser) {
//        let authStatus = MPMediaLibrary.authorizationStatus()
//        switch authStatus {
//        /// 未作出选择
//        case .notDetermined:
//            MPMediaLibrary.requestAuthorization { (status) in
//                if status == .authorized{
//                    DispatchQueue.main.async {
//                        clouser(true)
//                    }
//                }else{
//                    DispatchQueue.main.async {
//                        clouser(false)
//                    }
//                }
//            }
//        /// 用户明确拒绝此应用程序的授权,或在设置中禁用该服务。
//        case .denied:
//            clouser(false)
//        /// 该应用程序未被授权使用该服务。由于用户无法改变对该服务的主动限制。此状态,并且个人可能没有拒绝授权。
//        case .restricted:
//            clouser(false)
//        /// 已授权
//        case .authorized:
//            clouser(true)
//        /// 扩展以后可能有的状态,做保护措施
//        @unknown default:
//            clouser(false)
//        }
//    }
    
//    /**
//     联网权限
//
//     - parameters: action 权限结果闭包
//     */
//    class func authNetwork(clouser: @escaping AuthClouser) {
//
//        let reachabilityManager = NetworkReachabilityManager(host: "www.baidu.com")
//        switch reachabilityManager?.status {
//        case .reachable(.cellular):
//            clouser(true)
//        case .reachable(.ethernetOrWiFi):
//            clouser(true)
//        case .none:
//            clouser(false)
//        case .notReachable:
//            clouser(false)
//            //            let status = reachabilityManager?.flags
//            //            switch status {
//            //            case .none:
//            //                clouser(false)
//            //            case .some(.connectionAutomatic):
//            //                clouser(false)
//            //            case .some(.connectionOnDemand):
//            //                clouser(false)
//            //            case .some(.connectionOnTraffic):
//            //                clouser(false)
//            //            case .some(.connectionRequired):
//            //                clouser(false)
//            //            case .some(.interventionRequired):
//            //                clouser(false)
//            //            case .some(.isDirect):
//            //                clouser(false)
//            //            case .some(.isLocalAddress):
//            //                clouser(false)
//            //            case .some(.isWWAN):
//            //                clouser(false)
//            //            case .some(.reachable):
//            //                clouser(false)
//            //            case .some(.transientConnection):
//            //                clouser(false)
//            //            case .init(rawValue: 0):
//            //                clouser(false)
//            //            case .some(_):
//            //                clouser(false)
//        //            }
//        case .unknown:
//            clouser(false)
//        }
//    }
    
    /**
     相机权限
     
     - parameters: action 权限结果闭包
     */
    class func authCamera(clouser: @escaping AuthClouser) {
        let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
        switch authStatus {
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { (result) in
                if result{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .denied:
            clouser(false)
        case .restricted:
            clouser(false)
        case .authorized:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
    /**
     相册权限
     
     - parameters: action 权限结果闭包
     */
    class func authPhotoLib(clouser: @escaping AuthClouser) {
        let authStatus = PHPhotoLibrary.authorizationStatus()
        switch authStatus {
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization { (status) in
                if status == .authorized{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .denied:
            clouser(false)
        case .restricted:
            clouser(false)
        case .authorized:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
    /**
     麦克风权限
     
     - parameters: action 权限结果闭包
     */
    class func authMicrophone(clouser: @escaping AuthClouser) {
        let authStatus = AVAudioSession.sharedInstance().recordPermission
        switch authStatus {
        case .undetermined:
            AVAudioSession.sharedInstance().requestRecordPermission { (result) in
                if result{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .denied:
            clouser(false)
        case .granted:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
    //开启麦克风权限
    func openAudioSession() {
        let permissionStatus = AVAudioSession.sharedInstance().recordPermission
        if permissionStatus == AVAudioSession.RecordPermission.undetermined {
            AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
                //此处可以判断权限状态来做出相应的操作,如改变按钮状态
                if granted{
                    DispatchQueue.main.async {

                    }
                }else{
                    DispatchQueue.main.async {
                        let alertView = UIAlertView(title: "无法访问您的麦克风" , message: "请到设置 -> 隐私 -> 麦克风 ,打开访问权限", delegate: nil, cancelButtonTitle: "取消", otherButtonTitles: "好的")
                                        alertView.show()
                    }
                }
            }
        }
    }
    
    //是否开启麦克风
    func getPermission() -> Bool{
        let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio)
        return authStatus != .restricted && authStatus != .denied
    }
    
    /**
     定位权限
     
     - parameters: action 权限结果闭包(有无权限,是否第一次请求权限)
     */
    class func authLocation(clouser: @escaping ((Bool,Bool)->())) {
        let authStatus = CLLocationManager.authorizationStatus()
        switch authStatus {
        case .notDetermined:
            //由于IOS8中定位的授权机制改变 需要进行手动授权
            locationAuthManager.requestAlwaysAuthorization()
            locationAuthManager.requestWhenInUseAuthorization()
            let status = CLLocationManager.authorizationStatus()
            if  status == .authorizedAlways || status == .authorizedWhenInUse {
                DispatchQueue.main.async {
                    clouser(true && CLLocationManager.locationServicesEnabled(), true)
                }
            }else{
                DispatchQueue.main.async {
                    clouser(false, true)
                }
            }
        case .restricted:
            clouser(false, false)
        case .denied:
            clouser(false, false)
        case .authorizedAlways:
            clouser(true && CLLocationManager.locationServicesEnabled(), false)
        case .authorizedWhenInUse:
            clouser(true && CLLocationManager.locationServicesEnabled(), false)
        @unknown default:
            clouser(false, false)
        }
    }
    
//    /**
//     推送权限
//
//     - parameters: action 权限结果闭包
//     */
//    class func authNotification(clouser: @escaping AuthClouser){
//        UNUserNotificationCenter.current().getNotificationSettings(){ (setttings) in
//            switch setttings.authorizationStatus {
//            case .notDetermined:
//                UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .carPlay, .sound]) { (result, error) in
//                    if result{
//                        DispatchQueue.main.async {
//                            clouser(true)
//                        }
//                    }else{
//                        DispatchQueue.main.async {
//                            clouser(false)
//                        }
//                    }
//                }
//            case .denied:
//                clouser(false)
//            case .authorized:
//                clouser(true)
//            case .provisional:
//                clouser(true)
//            @unknown default:
//                clouser(false)
//            }
//        }
//    }
    
    /**
     运动与健身
     
     - parameters: action 权限结果闭包
     */
    class func authCMPedometer(clouser: @escaping AuthClouser){
        cmPedometer.queryPedometerData(from: Date(), to: Date()) { (pedometerData, error) in
            if pedometerData?.numberOfSteps != nil{
                DispatchQueue.main.async {
                    clouser(true)
                }
            }else{
                DispatchQueue.main.async {
                    clouser(false)
                }
            }
        }
    }
    
    /**
     通讯录权限
     
     - parameters: action 权限结果闭包
     */
    class func authContacts(clouser: @escaping AuthClouser){
        let authStatus = CNContactStore.authorizationStatus(for: .contacts)
        switch authStatus {
        case .notDetermined:
            CNContactStore().requestAccess(for: .contacts) { (result, error) in
                if result{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .restricted:
            clouser(false)
        case .denied:
            clouser(false)
        case .authorized:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
//    /**
//     Siri 权限
//     
//     - parameters: action 权限结果闭包
//     */
//    class func authSiri(clouser: @escaping AuthClouser){
//        let authStatus = INPreferences.siriAuthorizationStatus()
//        switch authStatus {
//        case .notDetermined:
//            INPreferences.requestSiriAuthorization { (status) in
//                if status == .authorized{
//                    DispatchQueue.main.async {
//                        clouser(true)
//                    }
//                }else{
//                    DispatchQueue.main.async {
//                        clouser(false)
//                    }
//                }
//            }
//        case .restricted:
//            clouser(false)
//        case .denied:
//            clouser(false)
//        case .authorized:
//            clouser(true)
//        @unknown default:
//            clouser(false)
//        }
//    }
    
    /**
     语音转文字权限
     
     - parameters: action 权限结果闭包
     */
    class func authSpeechRecognition(clouser: @escaping AuthClouser){
        if #available(iOS 10.0, *) {
            let authStatus = SFSpeechRecognizer.authorizationStatus()
            switch authStatus {
            case .notDetermined:
                SFSpeechRecognizer.requestAuthorization { (status) in
                    if status == .authorized{
                        DispatchQueue.main.async {
                            clouser(true)
                        }
                    }else{
                        DispatchQueue.main.async {
                            clouser(false)
                        }
                    }
                }
            case .restricted:
                clouser(false)
            case .denied:
                clouser(false)
            case .authorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        } else {
            // Fallback on earlier versions
        }
    }
    
    /**
     提醒事项
     
     - parameters: action 权限结果闭包
     */
    class func authRreminder(clouser: @escaping AuthClouser){
        let authStatus = EKEventStore.authorizationStatus(for: .reminder)
        switch authStatus {
        case .notDetermined:
            EKEventStore().requestAccess(to: .reminder) { (result, error) in
                if result{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .restricted:
            clouser(false)
        case .denied:
            clouser(false)
        case .authorized:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
    /**
     日历
     
     - parameters: action 权限结果闭包
     */
    class func authEvent(clouser: @escaping AuthClouser){
        let authStatus = EKEventStore.authorizationStatus(for: .event)
        switch authStatus {
        case .notDetermined:
            EKEventStore().requestAccess(to: .event) { (result, error) in
                if result{
                    DispatchQueue.main.async {
                        clouser(true)
                    }
                }else{
                    DispatchQueue.main.async {
                        clouser(false)
                    }
                }
            }
        case .restricted:
            clouser(false)
        case .denied:
            clouser(false)
        case .authorized:
            clouser(true)
        @unknown default:
            clouser(false)
        }
    }
    
    /**
     FaceID或者TouchID 认证
     
     - parameters: action 权限结果闭包
     */
    class func authFaceOrTouchID(clouser: @escaping ((Bool,Error)->())) {
        let context = LAContext()
        var error: NSError?
        let result = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
        if result {
            context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "认证") { (success, authError) in
                if success{
                    print("成功")
                }else{
                    print("失败")
                }
            }
        }else{
            /**
             #define kLAErrorAuthenticationFailed                       -1
             #define kLAErrorUserCancel                                 -2
             #define kLAErrorUserFallback                               -3
             #define kLAErrorSystemCancel                               -4
             #define kLAErrorPasscodeNotSet                             -5
             #define kLAErrorTouchIDNotAvailable                        -6
             #define kLAErrorTouchIDNotEnrolled                         -7
             #define kLAErrorTouchIDLockout                             -8
             #define kLAErrorAppCancel                                  -9
             #define kLAErrorInvalidContext                            -10
             #define kLAErrorNotInteractive                          -1004
             
             #define kLAErrorBiometryNotAvailable              kLAErrorTouchIDNotAvailable
             #define kLAErrorBiometryNotEnrolled               kLAErrorTouchIDNotEnrolled
             
             */
            print("不可以使用")
        }
    }
    
    /**
     健康  (写:体能训练、iOS13 听力图 读: 健身记录、体能训练、iOS13 听力图)
     
     - parameters: action 权限结果闭包
     */
    class func authHealth(clouser: @escaping AuthClouser){
        if HKHealthStore.isHealthDataAvailable(){
            let authStatus = HKHealthStore().authorizationStatus(for: .workoutType())
            switch authStatus {
            case .notDetermined:
                if #available(iOS 13.0, *) {
                    HKHealthStore().requestAuthorization(toShare: [.audiogramSampleType(), .workoutType()], read: [.activitySummaryType(), .workoutType(), .audiogramSampleType()]) { (result, error) in
                        if result{
                            DispatchQueue.main.async {
                                clouser(true)
                            }
                        }else{
                            DispatchQueue.main.async {
                                clouser(false)
                            }
                        }
                    }
                } else {
                    if #available(iOS 9.3, *) {
                        HKHealthStore().requestAuthorization(toShare: [.workoutType()], read: [.activitySummaryType(), .workoutType()]) { (result, error) in
                            if result{
                                DispatchQueue.main.async {
                                    clouser(true)
                                }
                            }else{
                                DispatchQueue.main.async {
                                    clouser(false)
                                }
                            }
                        }
                    } else {
                        // Fallback on earlier versions
                    }
                }
            case .sharingDenied:
                clouser(false)
            case .sharingAuthorized:
                clouser(true)
            @unknown default:
                clouser(false)
            }
        }else{
            clouser(false)
        }
    }
    
    /**
     家庭、住宅数据
     
     - parameters: action 权限结果闭包
     */
    class func authHomeKit(clouser: @escaping AuthClouser) {
        if #available(iOS 13.0, *) {
            switch HMHomeManager().authorizationStatus {
            case .authorized:
                clouser(true)
            case .determined:
                clouser(false)
            case .restricted:
                clouser(false)
            default:
                clouser(false)
            }
        } else {
            if (HMHomeManager().primaryHome != nil) {
                clouser(true)
            }else{
                clouser(false)
            }
        }
    }
    
    /**
     系统设置
     
     - parameters: urlString 可以为系统,也可以为微信:weixin://、QQ:mqq://
     - parameters: action 结果闭包
     */
    class func authSystemSetting(urlString :String?, clouser: @escaping AuthClouser) {
        var url: URL
        if (urlString != nil) && urlString?.count ?? 0 > 0 {
            url = URL(string: urlString!)!
        }else{
            url = URL(string: UIApplication.openSettingsURLString)!
        }
        
        if UIApplication.shared.canOpenURL(url){
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:]) { (result) in
                    if result{
                        clouser(true)
                    }else{
                        clouser(false)
                    }
                }
            } else {
                // Fallback on earlier versions
            }
        }else{
            clouser(false)
        }
    }
}

  1. 重点来了 录制完音频文件后,也是跟前端定义好方法返回回去
    让前端利用base64编码发送回去给前端,剩下他就能接收处理
let fileData = try! Data(contentsOf: wavFileURL)
      //将图片转为base64编码
let base64 = fileData.base64EncodedString(options: .endLineWithLineFeed).addingPercentEncoding(withAllowedCharacters: .alphanumerics)
UserDefaults.standard.setValue(base64, forKey: "wavFileURL")

4.到这里,你就以为很成功了,能顺利录音-播放-展示到webview了吗?那你就继续入坑吧
原本以为我这里通过base64把录完音的文件发送给前端就没我活了,谁知道录完音老是播放不了
解决方法:

  1. 由于WKWebView无权限访问本地文件,访问本地文件使用的是file://协议,由于WKWebView的安全机制,会报一些错无法访问到。需要打开webView的file://协议访问权限,设置allowFileAccessFromFileURLs为true。

2.如果出现跨域的报错,也可以通过设置allowUniversalAccessFromFileURLs为true来解决。

WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 解决HTML请求跨域
[config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];

WKPreferences *preferences = [[WKPreferences alloc] init];
// 打开web访问本地文件权限
[preferences setValue:@(true) forKey:@"allowFileAccessFromFileURLs"];
config.preferences = preferences;

let preference = WKPreferences()
        preference.minimumFontSize = 0
        preference.javaScriptEnabled = true
        preference.javaScriptCanOpenWindowsAutomatically = true
        preference.setValue("TRUE", forKey: "allowFileAccessFromFileURLs")
        debugPrint("这里已经进来了")
image.png

更多方法交流可以家魏鑫:lixiaowu1129,一起探讨iOS相关技术!

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

推荐阅读更多精彩内容