什么是CodeReview及CodeReview的目的

  1. CodeReview 不是针对某个人的批评会,也不是翻旧账的行为。CodeReview 是为了规范团队编码规范,提高整体代码质量,培养良好的编码习惯而采取的一种手段。
  2. 主要目的是推动团队成员遵守被按规范执行编码规范,发现代码工程中的异味代码和不遵守规范的代码,强调规范,提升个人意思。
  3. Coding 可以看成是一项工程,是多个人通力合作的产出。任何小的细节遗漏或没做好都可能导致线上故障。所有,要对每一行代码负责。
  4. 代码的可读性和易维护性是对每一个工程师的基本要求。写出来的代码是为了后续更容易的维护和功能的更迭,不仅仅只是能运行。
  5. Laravel 编码规范见文档

Talk is cheap show me the code.

Middleware codereview

文件 app/Exceptions/Handler.php

  1. <?php
  2. /**
  3. * Report or log an exception.
  4. *
  5. * @param \Exception $exception
  6. * @return void
  7. */
  8. public function report(Exception $exception)
  9. { // 非配置文件代码中,不要直接使用 env 函数获取 .env 配置
  10. if (env('APP_ENV') !== 'local') {
  11. // 此处的 if 可以提升,合并到上一级,可读性更好
  12. if (app()->bound('sentry') && $this->shouldReport($exception)) {
  13. app('sentry')->captureException($exception);
  14. }
  15. }
  16. parent::report($exception);
  17. }

根据智众 Laravel 规范 以及 SRP 原则重构后如下。

  1. <?php
  2. /**
  3. * Report or log an exception.
  4. *
  5. * @param \Exception $exception
  6. * @return void
  7. */
  8. public function report(Exception $exception)
  9. {
  10. if ($this->shouldReportToSentry($exception)) {
  11. app('sentry')->captureException($exception);
  12. }
  13. parent::report($exception);
  14. }
  15. // 判断是否应该的函数最好以 is should can could 等开头
  16. // SRP 原则,最小责任。
  17. // 将判断是否应该被sentry捕获的异常单独提出来作为一个函数
  18. public function shouldReportToSentry(Exception $exception): bool
  19. {
  20. !\in_array(config('app.env'), ['production', 'staging', 'prod']) &&
  21. app()->bound('sentry') &&
  22. $this->shouldReport($exception);
  23. }

文件 app/Http/Middleware/CheckSimpleSign.php

  1. <?php
  2. namespace App\Http\Middleware;
  3. use App\Models\ZOauthClients;
  4. use App\Traits\ResponseFormat;
  5. use App\Utils\Sign;
  6. use Carbon\Carbon;
  7. use Closure;
  8. use Illuminate\Support\Facades\Cache;
  9. class CheckSimpleSign
  10. {
  11. use ResponseFormat;
  12. protected $response;
  13. const MAX_TIMESTAMP_SKEW = 60;
  14. const CACHE_KEY="mmc:client_id:";
  15. /**
  16. * Handle an incoming request.
  17. *
  18. * @param \Illuminate\Http\Request $request
  19. * @param \Closure $next
  20. * @return mixed
  21. */
  22. public function handle($request, Closure $next)
  23. {
  24. // try 的异常是什么异常?
  25. try{
  26. // 变量命名规范 参考 PSR2
  27. // 小写字母开头的驼峰写法
  28. $_client_id=$request->input('_client_id');
  29. $_timestamp=$request->input('_timestamp');
  30. $_sign=$request->input('_sign');
  31. //兼容未传验参参数无需校验
  32. if(!$_client_id && !$_timestamp && !$_sign){
  33. return $next($request);
  34. }
  35. if(!$_client_id || !$_timestamp || !$_sign){
  36. return $this->jsonError(421, '缺少参数',0);
  37. }
  38. // 无意义
  39. if(strtotime(date("Ymdhis",$_timestamp))!=$_timestamp){
  40. return $this->jsonError(421, '参数异常',0);
  41. }
  42. $version=$this->_client_version($_client_id);
  43. if(!$version){
  44. return $this->jsonError(421, '参数异常.',0);
  45. }
  46. $signParams=[
  47. "_client_id"=>$_client_id,
  48. "_timestamp"=>$_timestamp,
  49. ];
  50. $sign=Sign::getSign($signParams,$version);
  51. if($sign!=$_sign){
  52. return $this->jsonError(421, '签名异常.',0);
  53. }
  54. return $next($request);
  55. }catch (\Exception $e){
  56. return $this->jsonError(421, $e->getMessage(),0);
  57. }
  58. }
  59. // 函数名命名
  60. private function _client_version($client_id){
  61. $cache_key=self::CACHE_KEY.$client_id;
  62. $version=Cache::get($cache_key);
  63. if($version){
  64. return $version;
  65. }
  66. $client_info=ZOauthClients::getInfoByClientId($client_id);
  67. if(empty($client_info)){
  68. return "";
  69. }
  70. Cache::add($cache_key,$client_info['version'],self::MAX_TIMESTAMP_SKEW);
  71. return $client_info['version'];
  72. }
  73. }

Controller codereview

文件 app/Http/Controllers/Api/MMC/BloodSugarController.php

  1. <?php
  2. //获取医院中心编号 部门暂不加入
  3. private function _getHospCd($hospId) {
  4. $sql = "select * from z_mmc_no_range where hosp_id = '$hospId' and mmc_min != '' limit 1";
  5. $ret = DBSelectOne($sql);
  6. if (isset($ret['mmc_min']) && $ret['mmc_min']) {
  7. return $ret['mmc_min'];
  8. }
  9. return false;
  10. }

操作DB,不要直接拼接 SQL 。要使用Laravel中ORM提供的写法。

大部分 Controller 没有分层。业务逻辑代码,大段堆积在 Controller 中。 Controller 职责过重。至少拆分一层 Service 层。Service 负责处理具体业务逻辑。Controller 仅仅负责分发客户端请求到Request验证参数,然后将具体处理业务逻辑分发到Service,获取并放回 Service 处理结果。对DB的操作建议写在Model中,如有必要,也可以写在Service中。

app/Http/Controllers/Api/Auth/WinAppAuthController.php

app/Http/Controllers/Api/Config/SysConfigController.php

app/Http/Controllers/Api/EyeGround/AuthController.php

app/Http/Controllers/Api/EyeGround/MmcEyesGroundController.php

app/Http/Controllers/Api/EyeGround/PatientController.php

app/Http/Controllers/Api/EyeGround/SystemNoticeController.php

app/Http/Controllers/Api/EyeGround/ViewEyesController.php

app/Http/Controllers/Api/EyeGround/VistelController.php

app/Http/Controllers/Api/MMC/BgController.php

app/Http/Controllers/Api/MMC/BloodSugarController.php

Utils 及 Service codereview

app/Utils/CommonUnits.php 工具类功能也要内聚。既然是 OOP 需要有 OO 的概念。需要具体拆分。

  1. <?php
  2. namespace App\Utils;
  3. use Illuminate\Support\Carbon;
  4. class CommonUnits
  5. {
  6. /**
  7. * 根据生日获取年龄
  8. * @param string $birth 生日
  9. * @param string $nowDate 当前日期
  10. * @param int $type 1周岁 2虚岁
  11. * @return int|null
  12. */
  13. public static function getAgeByBirth($birth,$nowDate="",$type=1){
  14. if(!$nowDate) $nowDate=Carbon::now()->format("Y-m-d");
  15. $birthTime = strtotime($birth);
  16. $nowTime = strtotime($nowDate);
  17. if($birthTime === false || $nowTime===null || $birthTime<0 || $nowTime<0){
  18. return null;
  19. }
  20. if($birthTime>$nowTime){
  21. return null;
  22. }
  23. list($birthYear, $birthMonth, $birthDay) = explode('-', $birth);
  24. list($currentYear, $currentMonth, $currentDay) = explode('-', $nowDate);
  25. if($type==1) {//周岁
  26. $age = $currentYear - $birthYear - 1;
  27. if ($currentMonth > $birthMonth || $currentMonth == $birthMonth && $currentDay >= $birthDay){
  28. $age++;
  29. }
  30. }else if($type==2){//虚岁
  31. $age = $currentYear - $birthYear;
  32. }
  33. return $age;
  34. }
  35. /**
  36. * @param $date
  37. * @param string $format
  38. * @return null|string
  39. */
  40. public static function getDateByDateTime($date,$format="Y-m-d"){
  41. $dateTime=strtotime($date);
  42. if(!$dateTime || $dateTime<0){
  43. return null;
  44. }
  45. return Carbon::createFromFormat($format,$date)->format($format);
  46. }
  47. /**
  48. * 数组笛卡尔集组合
  49. * @param $arr
  50. * @return array|mixed
  51. * @example CommonUnits::combineDika([[1,2], [3,4,5]]);
  52. */
  53. public static function combineDika($arr)
  54. {
  55. $result = array_shift($arr);
  56. while ($arr2 = array_shift($arr)) {
  57. $arr1 = $result;
  58. $result = array();
  59. foreach ($arr1 as $v) {
  60. foreach ($arr2 as $v2) {
  61. if (!is_array($v)) $v = array($v);
  62. if (!is_array($v2)) $v2 = array($v2);
  63. $result[] = array_merge_recursive($v, $v2);
  64. }
  65. }
  66. }
  67. return $result;
  68. }
  69. /**
  70. * 数据脱敏
  71. * @param $string 需要脱敏值
  72. * @param int $start 开始
  73. * @param int $length 结束
  74. * @param string $re 脱敏替代符号
  75. * @return bool|string
  76. * 例子:
  77. * dataDesensitization('18811113683', 3, 4); //188****3683
  78. * dataDesensitization('乐杨俊', 0, -1); //**俊
  79. */
  80. public static function dataDesensitization($string, $start = 0, $length = 0, $re = '*')
  81. {
  82. if (empty($string)){
  83. return false;
  84. }
  85. $strarr = array();
  86. $mb_strlen = mb_strlen($string);
  87. while ($mb_strlen) {//循环把字符串变为数组
  88. $strarr[] = mb_substr($string, 0, 1, 'utf8');
  89. $string = mb_substr($string, 1, $mb_strlen, 'utf8');
  90. $mb_strlen = mb_strlen($string);
  91. }
  92. $strlen = count($strarr);
  93. $begin = $start >= 0 ? $start : ($strlen - abs($start));
  94. $end = $last = $strlen - 1;
  95. if ($length > 0) {
  96. $end = $begin + $length - 1;
  97. } elseif ($length < 0) {
  98. $end -= abs($length);
  99. }
  100. for ($i = $begin; $i <= $end; $i++) {
  101. $strarr[$i] = $re;
  102. }
  103. if($start>0) {
  104. if ($begin >= $end || $begin >= $last || $end > $last) return false;
  105. }else if($start===0){
  106. if ($begin > $end || $begin >= $last || $end > $last) return false;
  107. }
  108. return implode('', $strarr);
  109. }

该类包含,根据生日获取年龄、数组笛卡尔集组合、数据脱敏、以及根据日期字符串获取date对象。

需要适当的对功能进行区分,来小心的构造类对象。不要所有不相干的功能都堆叠在一个类中。

日期相干的功能使用 Carbon\Carbon 库应该可以满足 90% 以上的需求,其他的也可以用 Carbon 中的方法来具体实现。

app/Services/YanDiNotMmcCompleteService.php

存在大段函数(170行)来实现一个功能。这样的函数大都可以拆分成更具体的职责函数来实现。大段函数,如果在功能更迭时需要改动,代码阅读成本以及测试成本很高,且容易引入bug。

  1. <?php
  2. public function completion($yanDiInfo)
  3. {
  4. Log::info("开始计算", $yanDiInfo);
  5. $userId = $yanDiInfo['user_id'];
  6. $hospId = $yanDiInfo['hosp_id'];
  7. $deptId = $yanDiInfo['dept_id'];
  8. $picture = json_decode($yanDiInfo['img_ids'], true);
  9. $left_num = 0;
  10. $right_num = 0;
  11. if (isset($picture['left'])) {
  12. $left_num = count($picture['left']);
  13. }
  14. if (isset($picture['right'])) {
  15. $right_num = count($picture['right']);
  16. }
  17. $isMmc = 0;
  18. $scores = [
  19. 'pictureScore' => 0, //眼底图片得分
  20. 'baseScore' => 0, //基本信息得分
  21. 'historyScore' => 0,//病史信息得分
  22. 'inspectScore' => 0,//实验室检查必填得分
  23. 'inspectNotRequiredScore' => 0, //实验室检查选填得分
  24. 'urineScore' => 0, // 实验室检查选填-尿常规得分
  25. ];
  26. ## 第一步 眼底图片上传 20分
  27. $scores['pictureScore'] = sizeof($picture) == 1 ? 10 : 20;
  28. ## (此为糖网问卷,mmc问卷相关需要单独查询出来)
  29. $questionnaireAnswer = TwUserNewSurvey::query()->from('z_user as u')
  30. ->selectRaw('s.*,u.name,u.sex,u.bday,u.cell,7 as company_id')
  31. ->leftJoin('tw_user_new_survey as s', 's.user_id', '=', 'u.id')
  32. ->where('u.id', $userId)->get()->toArray()[0] ?? [];
  33. //多选题组装
  34. $multipleArr = [];
  35. foreach ($questionnaireAnswer as $k => $v) {
  36. if ($questionnaireAnswer[$k] && $questionnaireAnswer[$k] !== '0.00') {
  37. // 处理每种情况数据
  38. if (array_key_exists($k, self::MULTIPLE_MAPPING)) {
  39. $res = json_decode($questionnaireAnswer[$k], true);
  40. if (is_array($res)) {
  41. foreach ($res as $val) {
  42. $multipleArr[$k . $val] = self::MULTIPLE_MAPPING[$k][$val] ?? '';
  43. }
  44. } else {
  45. $multipleArr[$k . $questionnaireAnswer[$k]] = self::MULTIPLE_MAPPING[$k][$questionnaireAnswer[$k]] ?? '';
  46. }
  47. }
  48. //处理问卷中字典
  49. if (array_key_exists($k, self::TW_ANSWER_MAPPING)) {
  50. $questionnaireAnswer[$k] = $this->twTransAnswer($questionnaireAnswer[$k], self::TW_ANSWER_MAPPING[$k]);
  51. }
  52. }else{
  53. // 排除 sex 0 男 1 女
  54. if ($k !== 'sex') {
  55. // 设置默认值为 ''
  56. $questionnaireAnswer[$k] = '';
  57. }
  58. }
  59. }
  60. ## 遍历问卷答案计算得分
  61. foreach ($questionnaireAnswer as $k => $v) {
  62. ## 基本信息
  63. if (array_key_exists($k, self::BASE_SCORE)) {
  64. if ($k === 'sex') {
  65. $scores['baseScore'] += self::BASE_SCORE[$k];
  66. continue;
  67. }
  68. if ($v && $v !== '0.00' && $v !== '0000-00-00') {
  69. $scores['baseScore'] += self::BASE_SCORE[$k];
  70. }
  71. }
  72. ## 病史信息
  73. if (array_key_exists($k, self::HISTORY_SCORE)) {
  74. if ($v && $v !== '[]') {
  75. $scores['historyScore'] += self::HISTORY_SCORE[$k];
  76. }
  77. }
  78. ## 实验室检查
  79. if (array_key_exists($k, self::INSPECT_REQUIRED_SCORE)) {
  80. if ($v && $v !== '[]' && $v !== '0.00') {
  81. $scores['inspectScore'] += self::INSPECT_REQUIRED_SCORE[$k];
  82. }
  83. }
  84. if (array_key_exists($k, self::INSPECT_NOT_REQUIRED_SCORE)) {
  85. if (in_array($k, ['pro', 'glu', 'leu', 'ery', 'nit']) && $v) {
  86. $scores['urineScore'] = 2;
  87. continue;
  88. }
  89. if ($v && $v !== '0.00') {
  90. $scores['inspectNotRequiredScore'] += self::INSPECT_NOT_REQUIRED_SCORE[$k];
  91. }
  92. }
  93. }
  94. Log::info("得分" . $userId .'----'. $scores['baseScore']);
  95. // 实验室检查选填得分,最多获得10分
  96. $scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] + $scores['urineScore'];
  97. $scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] > 10 ? 10 : $scores['inspectNotRequiredScore'];
  98. // 实验室检查总得分
  99. $scores['inspectTotalScore'] = $scores['inspectScore'] + $scores['inspectNotRequiredScore'];
  100. $completeTotal = $scores['pictureScore'] + $scores['baseScore'] + $scores['historyScore'] + $scores['inspectTotalScore'];
  101. $insertData = [
  102. 'user_id' => $userId,
  103. 'hosp_id' => $hospId,
  104. 'dept_id' => $deptId,
  105. 'is_mmc' => $isMmc,
  106. 'complete_picture' => $scores['pictureScore'],
  107. 'complete_base' => $scores['baseScore'],
  108. 'complete_history' => $scores['historyScore'],
  109. 'complete_inspect' => $scores['inspectTotalScore'],
  110. 'complete_total' => $completeTotal,
  111. 'complete_inspect_not_require' => $scores['inspectNotRequiredScore'],
  112. ];
  113. YanDiComplete::query()->updateOrCreate(['user_id' => $userId, 'dept_id' => $deptId], $insertData);
  114. Log::info("sex". $questionnaireAnswer['sex']);
  115. $insertAnswerData = [
  116. 'no' => date("Ymd"),
  117. 'hosp_id' => $yanDiInfo['hosp_id'],
  118. 'hosp_name' => $yanDiInfo['hosp_name'],
  119. 'dept_name' => $yanDiInfo['dept_name'],
  120. 'is_mmc' => $isMmc ? '是' : '否',
  121. 'user_id' => $userId,
  122. 'name' => $questionnaireAnswer['name'] ?? '',//姓名
  123. 'sex' => isset($questionnaireAnswer['sex']) ? ([0 => '男', 1 => '女'][$questionnaireAnswer['sex']] ?? '未知') : '未知',//性别
  124. 'bday' => $questionnaireAnswer['bday'] ?? '',//'出生日期',
  125. 'cell' => $questionnaireAnswer['cell'] ?? '',//'手机号',
  126. 'has_cell' => isset($questionnaireAnswer['cell']) && $questionnaireAnswer['cell'] ? '是' : '否',//'是否手机号验证',
  127. 'height' => $questionnaireAnswer['height'] ?? '',//'身高',
  128. 'weight' => $questionnaireAnswer['weight'] ?? '',//'体重',
  129. 'sbp' => $questionnaireAnswer['sbp'] ?? '',//'收缩压',
  130. 'dbp' => $questionnaireAnswer['dbp'] ?? '',//'舒张压',
  131. 'education' => $questionnaireAnswer['education'] ?? '',//'教育',
  132. 'province' => $this->getProvinceName($questionnaireAnswer['province'] ?? ''),//'目前的长期居住地',
  133. 'left_num' => $left_num, // 左眼图片数量
  134. 'right_num' => $right_num, // 右眼图片数量
  135. 'is_tnb' => $questionnaireAnswer['is_tnb'] ?? '',//'糖尿病史',
  136. 'tnb_start_time' => $questionnaireAnswer['tnb_start_time'] ?? '', //糖尿病开始时间
  137. 'is_high_blood' => $questionnaireAnswer['is_high_blood'] ?? '',//'高血压史',
  138. 'high_blood_start_time' => $questionnaireAnswer['high_blood_start_time'] ?? '',//'高血压开始时间',
  139. 'other_jzxjb_type' => $questionnaireAnswer['other_jzxjb_type'] ?? '',
  140. 'is_zfg' => $questionnaireAnswer['is_zfg'], //脂肪肝
  141. 'is_jzxjb' => $questionnaireAnswer['is_jzxjb'], //甲状腺疾病
  142. 'glu0' => $questionnaireAnswer['glu0'] ?? '',//'空腹血糖',
  143. 'hba1c' => $questionnaireAnswer['hba1c'] ?? '',//'糖化血红蛋白'
  144. 'cr' => $questionnaireAnswer['cr'] ?? '',//'血肌酐',
  145. 'ua' => $questionnaireAnswer['ua'] ?? '',//'血尿酸',
  146. 'tg' => $questionnaireAnswer['tg'] ?? '',//'甘油三酯TG',
  147. 'tc' => $questionnaireAnswer['tc'] ?? '',//'高密度脂蛋白胆固醇HDL-c',
  148. 'hdlc' => $questionnaireAnswer['hdlc'] ?? '',//'低密度脂蛋白胆固醇LDL-c',
  149. 'ldlc' => $questionnaireAnswer['ldlc'] ?? '',//'总胆固醇TC',
  150. 'record_at' => $yanDiInfo['created_at'],
  151. ];
  152. $insertAnswerData = array_merge($insertAnswerData, $multipleArr);
  153. Log::info("aa", $insertAnswerData);
  154. //dd($insertAnswerData);
  155. YanDiAnswer::query()->where([
  156. 'no' => date("Ymd"),
  157. 'user_id' => $userId,
  158. 'dept_id' => $deptId,
  159. ])->delete();
  160. YanDiAnswer::query()->updateOrCreate([
  161. 'no' => date("Ymd"),
  162. 'user_id' => $userId,
  163. 'dept_id' => $deptId,
  164. ], $insertAnswerData);
  165. return true;
  166. }

重构,将子步骤抽取出来,单独成为函数,做到操作清晰,调用明确。

  1. <?php
  2. public function calculateCompletionScore($eyesInfo)
  3. {
  4. // 获取用户问卷填写详情
  5. $answers = $this->getUserAnswers($eyesInfo['user_id']);
  6. // 按照计算的方式组装处理 answer
  7. $answers = $this->rebuildAnswerStructure($answers);
  8. // 遍历计算完成度分数
  9. $scores = $this->calculateScoreOfAnswers($answers);
  10. ...
  11. 更新相关表结构
  12. }

除必要的注释外,被注释的不必要代码。一定不要带入生产环境。要删除干净,保持代码的洁净。

总结

  1. 代码分层要清晰,Controller 职责的接受 Request 并将验证的 Request 派送到 Service 层进行业务逻辑处理。Controller 一定不可以包含对业务逻辑的处理。具体业务逻辑的处理一定要在 Service 层编码。
  2. 代码职责问题。一个函数只允许有一个单一职责,如果代码函数超过 100行,一般都可以进行拆分和提炼出更小职责的函数。SRP 原则能有益于后续维护和需求变更。且如果有单元测试覆盖。能更好的的实践单元测试。利于单元测试的代码的编写。
  3. 一个项目的开发不是一个人的事情,是一个团队的事情。所以,养成良好的开发习惯和团队规范是必要的事情。这样才能持续写出易维护、运行稳定的代码。
  4. 具体的类的职责也不要超过类本身的职责。CommonUtils 就是一个错误的 OOP 抽象示范。
  5. 命名规范可以通过工具来保证。保持一致的命名规范和代码格式,是工程化的前提。
  6. 不允许直接拼接 SQL。使用 ORM 来进行数据库的操作。不允许在循环体中,进行数据库操作。会造成不可控的运行时间。
  7. 单例模式不一定使用于所有场景。不要盲目的 COPY 和应用。能静态声明(static)的方法可以尽量静态声明。
  8. 不要节省代码注释,你能读懂的,不代表所有人都能懂原来Code的意图。更多的注释能帮助更容易读懂代码。