restful api
传统api
- 获取用户信息 get /api/user/read
- 更新用户信息 post /api/user/updat
- 新增用户信息 post/api/user/add
- 删除用户信息 post/api/user/delete
restful api
- 获取用户信息 get /api/user/1
- 更新用户信息put /api/user/1
- 新增用户信息 post /api/user
- 删除用户信息deletean/api/user/1
API数据结构格式
- HTTP状态码使用TK自带的json实现
- status 业务状态码
- message 提示信息
- data 数据层
function show($status, $message, $data=[], $httpCode=200) {
    $data = [
        'status' => $status,
        'message' => $message,
        'data' => $data,
    ];
    return json($data, $httpCode);
}
不可预知的内部异常api数据输出解决方案
- TP5是通过thinkphp-library-think-exception-Handle.php-render来呈现异常数据(但是客户端识别不到这种异常)
- config配置exception_handle填写异常类路径
- 'exception_handle' => '\app\common\lib\exception\ApiHandleException',
 
class ApiHandleException extends  Handle {
    /**
     * http 状态码
     * @var int
     */
    public $httpCode = 500;
    public function render(\Exception $e) {
        // 还原正常报错,上线后为flase(服务端开发)
        if(config('app_debug') == true) {
            return parent::render($e);
        }
        if ($e instanceof ApiException) {
            $this->httpCode = $e->httpCode;
        }
        return  show(0, $e->getMessage(), [], $this->httpCode);
    }
}
class ApiException extends Exception {
    public $message = '';
    public $httpCode = 500;
    public $code = 0;
    /**
     * @param string $message
     * @param int $httpCode
     * @param int $code
     */
    public function __construct($message = '', $httpCode = 0, $code = 0) {
        $this->httpCode = $httpCode;
        $this->message = $message;
        $this->code = $code;
    }
}
API数据安全
- 背景
- 接口请求地址和参数暴露
- 重要接口返回数据明文暴露
- APP登录态请求的数据完全性问题
- 代码层的数据完全问题
 
- 解决方式就是各种的加密:MD5 AES(对称加密) RSA(非对称,效率较低)
- 基本参数放入header
- 每次http请求都携带sign (有效时间,唯一性)
- sign唯一性保证
- 请求参数、返回数据按安全性适当加密
- access token
- 客户端和服务器端时间不一致性解决方案
- 解决:获取服务端时间,客户端拿到服务端正确时间进行对比。然后如果有差值,然后把差值在计算的时候补上
 
 
/**
     * 生成每次请求的sign
     * @param array $data
     * @return string
     */
    public static function setSign($data = []) {
        // 1 按字段排序
        ksort($data);
        // 2拼接字符串数据  &
        $string = http_build_query($data);
        // 3通过aes来加密
        $string = (new Aes())->encrypt($string);
        return $string;
    }
/**
     * 检查sign是否正常
     * @param array $data
     * @param $data
     * @return boolen
     */
    public static function checkSignPass($data) {
        $str = (new Aes())->decrypt($data['sign']);
        if(empty($str)) {
            return false;
        }
        // diid=xx&app_type=3
        parse_str($str, $arr);
        if(!is_array($arr) || empty($arr['did'])
            || $arr['did'] != $data['did']
        ) {
            return false;
        }
        // 有效时间:时间间隔不能超过60s (时间使用13位时间戳,这样唯一性会比较强)
        if(!config('app_debug')) {
            if ((time() - ceil($arr['time'] / 1000)) > config('app.app_sign_time')) {
                return false;
            }
            //echo Cache::get($data['sign']);exit;
            // 唯一性判定
            if (Cache::get($data['sign'])) {
                return false;
            }
        }
        return true;
    }
/**
 * 检查每次app请求的数据是否合法
 */
    public function checkRequestAuth() {
        // 首先需要获取headers
        $headers = request()->header();
        // todo
        // sign 加密需要 客户端工程师 , 解密:服务端工程师
        // 1 headers body 仿照sign 做参数的加解密
        // 2
        //  3
        // 基础参数校验
        if(empty($headers['sign'])) {
            throw new ApiException('sign不存在', 400);
        }
        if(!in_array($headers['app_type'], config('app.apptypes'))) {
            throw new ApiException('app_type不合法', 400);
        }
        // 需要sign
        if(!IAuth::checkSignPass($headers)) {
            throw new ApiException('授权码sign失败', 401);
        }
        Cache::set($headers['sign'], 1, config('app.app_sign_cache_time'));
        // 存储方式 :1、文件  2、mysql 3、redis
        $this->headers = $headers;
    }
API接口文档编写(API入参,出参的格式)
- 有利于客户端工程师熟悉接口
- 有利于服务端工程师接手项目
- API接口地址
- 请求方式
- 入参格式
- 出参格式
- http code
APP版本升级
- APP更新迭代快
- APP是安装在用户手机设备上
- APP版本升级的类型
- 开设API接口
- APP识别接口做相关判定3
    /**
     * 客户端初始化接口
     * 1、检测APP是否需要升级
     */
    public function init() {
        // app_type 去ent_version 查询
        $version = model('Version')->getLastNormalVersionByAppType($this->headers['app_type']);
        if(empty($version)) {
            return new ApiException('error', 404);
        }
        if($version->version > $this->headers['version']) {
            $version->is_update = $version->is_force == 1 ? 2 : 1;
        }else {
            $version->is_update = 0;  // 0 不更新 , 1需要更新, 2强制更新
        }
        // 记录用户的基本信息 用于统计
        $actives = [
            'version' => $this->headers['version'],
            'app_type' => $this->headers['app_type'],
            'did' => $this->headers['did'],
        ];
        try {
            model('AppActive')->add($actives);
        }catch (\Exception $e) {
            // todo
            //Log::write();
        }
        return show(config('code.success'), 'OK', $version, 200);
    }
APP端异常、性能监控及定位分析
- 常见的移动端异常
- Crash在使用APP的过程中突然发生闪退现象
- 卡顿出现画面的卡顿
- Exception 程序被catch起来的exception
- ANR 出现提示无响应弹框(android)
 
- 端异常数据收集方案解剖(统计数据)
- Crash 卡顿Exception ANR次数
- 影响用户数
 
- 第三方成熟服务
- 听云
- oneapm
- 端APP只需要集成第三方平台提供的sdk
 
APP推送
点赞功能
/**
     * 新闻点赞功能开发
     * @return array
     */
    public function save() {
        $id = input('post.id', 0 , 'intval');
        if(empty($id)) {
            return show(config('code.error'), 'id不存在', [], 404);
        }
        // 判定这个 id的新闻文章  -> ent_news 小伙伴自行加入
        $data = [
            'user_id' => $this->user->id,
            'news_id' => $id,
        ];
        // 查询库里面是否存在 点赞
        $userNews = model('UserNews')->get($data);
        if($userNews) {
            return show(config('code.error'), '已经点赞过,不能再次点赞', [], 401);
        }
        try {
            $userNewsId = model('UserNews')->add($data);
            if($userNewsId) {
                model('News')->where(['id' => $id])->setInc('upvote_count');
                return show(config('code.success'), 'OK', [], 202);
            }else {
                return show(config('code.error'), '内部错误 点赞失败', [], 500);
            }
        }catch (\Exception $e) {
            return show(config('code.error'), '内部错误 点赞失败', [], 500);
        }
    }
    /**
     * 取消点赞
     */
    public function delete() {
        $id = input('delete.id', 0, 'intval');
        if(empty($id)) {
            return show(config('code.error'), 'id不存在', [], 404);
        }
        // 判定这个 id的新闻文章  -> ent_news 小伙伴自行加入
        $data = [
            'user_id' => $this->user->id,
            'news_id' => $id,
        ];
        // 查询库里面是否存在 点赞
        $userNews = model('UserNews')->get($data);
        if(empty($userNews)) {
            return show(config('code.error'), '没有被点赞过,无法取消', [], 401);
        }
        try {
            $userNewsId = model('UserNews')
                ->where($data)
                ->delete();
            if($userNewsId) {
                model('News')->where(['id' => $id])->setDec('upvote_count');
                return show(config('code.success'), 'OK', [], 202);
            }else {
                return show(config('code.error'), '取消失败', [], 500);
            }
        }catch (\Exception $e) {
            return show(config('code.error'), '内部错误 点赞失败', [], 500);
        }
    }
小知识点:
- 后台权限控制使用封装Base类库来达到统一管理的目的,_initialize
- 调试小技巧 : $this->getLastSql(); halt(); echo ** exit;
- mysql default null影响索引性能。
			
			打开App,阅读手记
		
	 
	
热门评论
- 
			  
				维生素P2018-12-09 0 
 
查看全部评论想问下为什么要 ApiException extends Exception,看起来你的代码里面没有看出拓展的功能,Exception类是自带 getMessage() 和 getCode() 的。
$e = new Exception('message',200);构造的时候可以塞进去这两个参数。