什么是CodeReview及CodeReview的目的
- CodeReview 不是针对某个人的批评会,也不是翻旧账的行为。CodeReview 是为了规范团队编码规范,提高整体代码质量,培养良好的编码习惯而采取的一种手段。
- 主要目的是推动团队成员遵守被按规范执行编码规范,发现代码工程中的异味代码和不遵守规范的代码,强调规范,提升个人意思。
- Coding 可以看成是一项工程,是多个人通力合作的产出。任何小的细节遗漏或没做好都可能导致线上故障。所有,要对每一行代码负责。
- 代码的可读性和易维护性是对每一个工程师的基本要求。写出来的代码是为了后续更容易的维护和功能的更迭,不仅仅只是能运行。
- Laravel 编码规范见文档
Talk is cheap show me the code.
Middleware codereview
文件 app/Exceptions/Handler.php
<?php
/**
* Report or log an exception.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{ // 非配置文件代码中,不要直接使用 env 函数获取 .env 配置
if (env('APP_ENV') !== 'local') {
// 此处的 if 可以提升,合并到上一级,可读性更好
if (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->captureException($exception);
}
}
parent::report($exception);
}
根据智众 Laravel 规范 以及 SRP 原则重构后如下。
<?php
/**
* Report or log an exception.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{
if ($this->shouldReportToSentry($exception)) {
app('sentry')->captureException($exception);
}
parent::report($exception);
}
// 判断是否应该的函数最好以 is should can could 等开头
// SRP 原则,最小责任。
// 将判断是否应该被sentry捕获的异常单独提出来作为一个函数
public function shouldReportToSentry(Exception $exception): bool
{
!\in_array(config('app.env'), ['production', 'staging', 'prod']) &&
app()->bound('sentry') &&
$this->shouldReport($exception);
}
文件 app/Http/Middleware/CheckSimpleSign.php
<?php
namespace App\Http\Middleware;
use App\Models\ZOauthClients;
use App\Traits\ResponseFormat;
use App\Utils\Sign;
use Carbon\Carbon;
use Closure;
use Illuminate\Support\Facades\Cache;
class CheckSimpleSign
{
use ResponseFormat;
protected $response;
const MAX_TIMESTAMP_SKEW = 60;
const CACHE_KEY="mmc:client_id:";
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// try 的异常是什么异常?
try{
// 变量命名规范 参考 PSR2
// 小写字母开头的驼峰写法
$_client_id=$request->input('_client_id');
$_timestamp=$request->input('_timestamp');
$_sign=$request->input('_sign');
//兼容未传验参参数无需校验
if(!$_client_id && !$_timestamp && !$_sign){
return $next($request);
}
if(!$_client_id || !$_timestamp || !$_sign){
return $this->jsonError(421, '缺少参数',0);
}
// 无意义
if(strtotime(date("Ymdhis",$_timestamp))!=$_timestamp){
return $this->jsonError(421, '参数异常',0);
}
$version=$this->_client_version($_client_id);
if(!$version){
return $this->jsonError(421, '参数异常.',0);
}
$signParams=[
"_client_id"=>$_client_id,
"_timestamp"=>$_timestamp,
];
$sign=Sign::getSign($signParams,$version);
if($sign!=$_sign){
return $this->jsonError(421, '签名异常.',0);
}
return $next($request);
}catch (\Exception $e){
return $this->jsonError(421, $e->getMessage(),0);
}
}
// 函数名命名
private function _client_version($client_id){
$cache_key=self::CACHE_KEY.$client_id;
$version=Cache::get($cache_key);
if($version){
return $version;
}
$client_info=ZOauthClients::getInfoByClientId($client_id);
if(empty($client_info)){
return "";
}
Cache::add($cache_key,$client_info['version'],self::MAX_TIMESTAMP_SKEW);
return $client_info['version'];
}
}
Controller codereview
文件 app/Http/Controllers/Api/MMC/BloodSugarController.php
<?php
//获取医院中心编号 部门暂不加入
private function _getHospCd($hospId) {
$sql = "select * from z_mmc_no_range where hosp_id = '$hospId' and mmc_min != '' limit 1";
$ret = DBSelectOne($sql);
if (isset($ret['mmc_min']) && $ret['mmc_min']) {
return $ret['mmc_min'];
}
return false;
}
操作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 的概念。需要具体拆分。
<?php
namespace App\Utils;
use Illuminate\Support\Carbon;
class CommonUnits
{
/**
* 根据生日获取年龄
* @param string $birth 生日
* @param string $nowDate 当前日期
* @param int $type 1周岁 2虚岁
* @return int|null
*/
public static function getAgeByBirth($birth,$nowDate="",$type=1){
if(!$nowDate) $nowDate=Carbon::now()->format("Y-m-d");
$birthTime = strtotime($birth);
$nowTime = strtotime($nowDate);
if($birthTime === false || $nowTime===null || $birthTime<0 || $nowTime<0){
return null;
}
if($birthTime>$nowTime){
return null;
}
list($birthYear, $birthMonth, $birthDay) = explode('-', $birth);
list($currentYear, $currentMonth, $currentDay) = explode('-', $nowDate);
if($type==1) {//周岁
$age = $currentYear - $birthYear - 1;
if ($currentMonth > $birthMonth || $currentMonth == $birthMonth && $currentDay >= $birthDay){
$age++;
}
}else if($type==2){//虚岁
$age = $currentYear - $birthYear;
}
return $age;
}
/**
* @param $date
* @param string $format
* @return null|string
*/
public static function getDateByDateTime($date,$format="Y-m-d"){
$dateTime=strtotime($date);
if(!$dateTime || $dateTime<0){
return null;
}
return Carbon::createFromFormat($format,$date)->format($format);
}
/**
* 数组笛卡尔集组合
* @param $arr
* @return array|mixed
* @example CommonUnits::combineDika([[1,2], [3,4,5]]);
*/
public static function combineDika($arr)
{
$result = array_shift($arr);
while ($arr2 = array_shift($arr)) {
$arr1 = $result;
$result = array();
foreach ($arr1 as $v) {
foreach ($arr2 as $v2) {
if (!is_array($v)) $v = array($v);
if (!is_array($v2)) $v2 = array($v2);
$result[] = array_merge_recursive($v, $v2);
}
}
}
return $result;
}
/**
* 数据脱敏
* @param $string 需要脱敏值
* @param int $start 开始
* @param int $length 结束
* @param string $re 脱敏替代符号
* @return bool|string
* 例子:
* dataDesensitization('18811113683', 3, 4); //188****3683
* dataDesensitization('乐杨俊', 0, -1); //**俊
*/
public static function dataDesensitization($string, $start = 0, $length = 0, $re = '*')
{
if (empty($string)){
return false;
}
$strarr = array();
$mb_strlen = mb_strlen($string);
while ($mb_strlen) {//循环把字符串变为数组
$strarr[] = mb_substr($string, 0, 1, 'utf8');
$string = mb_substr($string, 1, $mb_strlen, 'utf8');
$mb_strlen = mb_strlen($string);
}
$strlen = count($strarr);
$begin = $start >= 0 ? $start : ($strlen - abs($start));
$end = $last = $strlen - 1;
if ($length > 0) {
$end = $begin + $length - 1;
} elseif ($length < 0) {
$end -= abs($length);
}
for ($i = $begin; $i <= $end; $i++) {
$strarr[$i] = $re;
}
if($start>0) {
if ($begin >= $end || $begin >= $last || $end > $last) return false;
}else if($start===0){
if ($begin > $end || $begin >= $last || $end > $last) return false;
}
return implode('', $strarr);
}
该类包含,根据生日获取年龄、数组笛卡尔集组合、数据脱敏、以及根据日期字符串获取date对象。
需要适当的对功能进行区分,来小心的构造类对象。不要所有不相干的功能都堆叠在一个类中。
日期相干的功能使用 Carbon\Carbon 库应该可以满足 90% 以上的需求,其他的也可以用 Carbon 中的方法来具体实现。
app/Services/YanDiNotMmcCompleteService.php
存在大段函数(170行)来实现一个功能。这样的函数大都可以拆分成更具体的职责函数来实现。大段函数,如果在功能更迭时需要改动,代码阅读成本以及测试成本很高,且容易引入bug。
<?php
public function completion($yanDiInfo)
{
Log::info("开始计算", $yanDiInfo);
$userId = $yanDiInfo['user_id'];
$hospId = $yanDiInfo['hosp_id'];
$deptId = $yanDiInfo['dept_id'];
$picture = json_decode($yanDiInfo['img_ids'], true);
$left_num = 0;
$right_num = 0;
if (isset($picture['left'])) {
$left_num = count($picture['left']);
}
if (isset($picture['right'])) {
$right_num = count($picture['right']);
}
$isMmc = 0;
$scores = [
'pictureScore' => 0, //眼底图片得分
'baseScore' => 0, //基本信息得分
'historyScore' => 0,//病史信息得分
'inspectScore' => 0,//实验室检查必填得分
'inspectNotRequiredScore' => 0, //实验室检查选填得分
'urineScore' => 0, // 实验室检查选填-尿常规得分
];
## 第一步 眼底图片上传 20分
$scores['pictureScore'] = sizeof($picture) == 1 ? 10 : 20;
## (此为糖网问卷,mmc问卷相关需要单独查询出来)
$questionnaireAnswer = TwUserNewSurvey::query()->from('z_user as u')
->selectRaw('s.*,u.name,u.sex,u.bday,u.cell,7 as company_id')
->leftJoin('tw_user_new_survey as s', 's.user_id', '=', 'u.id')
->where('u.id', $userId)->get()->toArray()[0] ?? [];
//多选题组装
$multipleArr = [];
foreach ($questionnaireAnswer as $k => $v) {
if ($questionnaireAnswer[$k] && $questionnaireAnswer[$k] !== '0.00') {
// 处理每种情况数据
if (array_key_exists($k, self::MULTIPLE_MAPPING)) {
$res = json_decode($questionnaireAnswer[$k], true);
if (is_array($res)) {
foreach ($res as $val) {
$multipleArr[$k . $val] = self::MULTIPLE_MAPPING[$k][$val] ?? '';
}
} else {
$multipleArr[$k . $questionnaireAnswer[$k]] = self::MULTIPLE_MAPPING[$k][$questionnaireAnswer[$k]] ?? '';
}
}
//处理问卷中字典
if (array_key_exists($k, self::TW_ANSWER_MAPPING)) {
$questionnaireAnswer[$k] = $this->twTransAnswer($questionnaireAnswer[$k], self::TW_ANSWER_MAPPING[$k]);
}
}else{
// 排除 sex 0 男 1 女
if ($k !== 'sex') {
// 设置默认值为 ''
$questionnaireAnswer[$k] = '';
}
}
}
## 遍历问卷答案计算得分
foreach ($questionnaireAnswer as $k => $v) {
## 基本信息
if (array_key_exists($k, self::BASE_SCORE)) {
if ($k === 'sex') {
$scores['baseScore'] += self::BASE_SCORE[$k];
continue;
}
if ($v && $v !== '0.00' && $v !== '0000-00-00') {
$scores['baseScore'] += self::BASE_SCORE[$k];
}
}
## 病史信息
if (array_key_exists($k, self::HISTORY_SCORE)) {
if ($v && $v !== '[]') {
$scores['historyScore'] += self::HISTORY_SCORE[$k];
}
}
## 实验室检查
if (array_key_exists($k, self::INSPECT_REQUIRED_SCORE)) {
if ($v && $v !== '[]' && $v !== '0.00') {
$scores['inspectScore'] += self::INSPECT_REQUIRED_SCORE[$k];
}
}
if (array_key_exists($k, self::INSPECT_NOT_REQUIRED_SCORE)) {
if (in_array($k, ['pro', 'glu', 'leu', 'ery', 'nit']) && $v) {
$scores['urineScore'] = 2;
continue;
}
if ($v && $v !== '0.00') {
$scores['inspectNotRequiredScore'] += self::INSPECT_NOT_REQUIRED_SCORE[$k];
}
}
}
Log::info("得分" . $userId .'----'. $scores['baseScore']);
// 实验室检查选填得分,最多获得10分
$scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] + $scores['urineScore'];
$scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] > 10 ? 10 : $scores['inspectNotRequiredScore'];
// 实验室检查总得分
$scores['inspectTotalScore'] = $scores['inspectScore'] + $scores['inspectNotRequiredScore'];
$completeTotal = $scores['pictureScore'] + $scores['baseScore'] + $scores['historyScore'] + $scores['inspectTotalScore'];
$insertData = [
'user_id' => $userId,
'hosp_id' => $hospId,
'dept_id' => $deptId,
'is_mmc' => $isMmc,
'complete_picture' => $scores['pictureScore'],
'complete_base' => $scores['baseScore'],
'complete_history' => $scores['historyScore'],
'complete_inspect' => $scores['inspectTotalScore'],
'complete_total' => $completeTotal,
'complete_inspect_not_require' => $scores['inspectNotRequiredScore'],
];
YanDiComplete::query()->updateOrCreate(['user_id' => $userId, 'dept_id' => $deptId], $insertData);
Log::info("sex". $questionnaireAnswer['sex']);
$insertAnswerData = [
'no' => date("Ymd"),
'hosp_id' => $yanDiInfo['hosp_id'],
'hosp_name' => $yanDiInfo['hosp_name'],
'dept_name' => $yanDiInfo['dept_name'],
'is_mmc' => $isMmc ? '是' : '否',
'user_id' => $userId,
'name' => $questionnaireAnswer['name'] ?? '',//姓名
'sex' => isset($questionnaireAnswer['sex']) ? ([0 => '男', 1 => '女'][$questionnaireAnswer['sex']] ?? '未知') : '未知',//性别
'bday' => $questionnaireAnswer['bday'] ?? '',//'出生日期',
'cell' => $questionnaireAnswer['cell'] ?? '',//'手机号',
'has_cell' => isset($questionnaireAnswer['cell']) && $questionnaireAnswer['cell'] ? '是' : '否',//'是否手机号验证',
'height' => $questionnaireAnswer['height'] ?? '',//'身高',
'weight' => $questionnaireAnswer['weight'] ?? '',//'体重',
'sbp' => $questionnaireAnswer['sbp'] ?? '',//'收缩压',
'dbp' => $questionnaireAnswer['dbp'] ?? '',//'舒张压',
'education' => $questionnaireAnswer['education'] ?? '',//'教育',
'province' => $this->getProvinceName($questionnaireAnswer['province'] ?? ''),//'目前的长期居住地',
'left_num' => $left_num, // 左眼图片数量
'right_num' => $right_num, // 右眼图片数量
'is_tnb' => $questionnaireAnswer['is_tnb'] ?? '',//'糖尿病史',
'tnb_start_time' => $questionnaireAnswer['tnb_start_time'] ?? '', //糖尿病开始时间
'is_high_blood' => $questionnaireAnswer['is_high_blood'] ?? '',//'高血压史',
'high_blood_start_time' => $questionnaireAnswer['high_blood_start_time'] ?? '',//'高血压开始时间',
'other_jzxjb_type' => $questionnaireAnswer['other_jzxjb_type'] ?? '',
'is_zfg' => $questionnaireAnswer['is_zfg'], //脂肪肝
'is_jzxjb' => $questionnaireAnswer['is_jzxjb'], //甲状腺疾病
'glu0' => $questionnaireAnswer['glu0'] ?? '',//'空腹血糖',
'hba1c' => $questionnaireAnswer['hba1c'] ?? '',//'糖化血红蛋白'
'cr' => $questionnaireAnswer['cr'] ?? '',//'血肌酐',
'ua' => $questionnaireAnswer['ua'] ?? '',//'血尿酸',
'tg' => $questionnaireAnswer['tg'] ?? '',//'甘油三酯TG',
'tc' => $questionnaireAnswer['tc'] ?? '',//'高密度脂蛋白胆固醇HDL-c',
'hdlc' => $questionnaireAnswer['hdlc'] ?? '',//'低密度脂蛋白胆固醇LDL-c',
'ldlc' => $questionnaireAnswer['ldlc'] ?? '',//'总胆固醇TC',
'record_at' => $yanDiInfo['created_at'],
];
$insertAnswerData = array_merge($insertAnswerData, $multipleArr);
Log::info("aa", $insertAnswerData);
//dd($insertAnswerData);
YanDiAnswer::query()->where([
'no' => date("Ymd"),
'user_id' => $userId,
'dept_id' => $deptId,
])->delete();
YanDiAnswer::query()->updateOrCreate([
'no' => date("Ymd"),
'user_id' => $userId,
'dept_id' => $deptId,
], $insertAnswerData);
return true;
}
重构,将子步骤抽取出来,单独成为函数,做到操作清晰,调用明确。
<?php
public function calculateCompletionScore($eyesInfo)
{
// 获取用户问卷填写详情
$answers = $this->getUserAnswers($eyesInfo['user_id']);
// 按照计算的方式组装处理 answer
$answers = $this->rebuildAnswerStructure($answers);
// 遍历计算完成度分数
$scores = $this->calculateScoreOfAnswers($answers);
...
更新相关表结构
}
除必要的注释外,被注释的不必要代码。一定不要带入生产环境。要删除干净,保持代码的洁净。
总结
- 代码分层要清晰,Controller 职责的接受 Request 并将验证的 Request 派送到 Service 层进行业务逻辑处理。Controller 一定不可以包含对业务逻辑的处理。具体业务逻辑的处理一定要在 Service 层编码。
- 代码职责问题。一个函数只允许有一个单一职责,如果代码函数超过 100行,一般都可以进行拆分和提炼出更小职责的函数。SRP 原则能有益于后续维护和需求变更。且如果有单元测试覆盖。能更好的的实践单元测试。利于单元测试的代码的编写。
- 一个项目的开发不是一个人的事情,是一个团队的事情。所以,养成良好的开发习惯和团队规范是必要的事情。这样才能持续写出易维护、运行稳定的代码。
- 具体的类的职责也不要超过类本身的职责。CommonUtils 就是一个错误的 OOP 抽象示范。
- 命名规范可以通过工具来保证。保持一致的命名规范和代码格式,是工程化的前提。
- 不允许直接拼接 SQL。使用 ORM 来进行数据库的操作。不允许在循环体中,进行数据库操作。会造成不可控的运行时间。
- 单例模式不一定使用于所有场景。不要盲目的 COPY 和应用。能静态声明(static)的方法可以尽量静态声明。
- 不要节省代码注释,你能读懂的,不代表所有人都能懂原来Code的意图。更多的注释能帮助更容易读懂代码。