你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
树上有云

服务器端内部业务处理失败消息传递方式 0个回复 专栏 @ Javascript

树上有云 发布于 1 年前

场景:创建订单

实际流程:

终端调用(PC端、移动端APP、微信端、Web端)-->控制器 或 接口-->实际的业务处理-->控制器 或 接口-->终端做出相应处理(控制器可能是渲染对应页面; 接口返回 JSON数据)

业务处理类动作:

  1. 检查用户是否登陆
  2. 验证商品 ID、购买数量等参数
  3. 检查该商品是否处于上架中
  4. 检查该商品是否可以购买
  5. 各种检查...
  6. 创建订单
  7. 记录 Log
  8. 返回订单创建结果给调用者
    1. 创建失败:...
    2. 创建成功:[return true|return Order info]

游戏规则

前后端数据格式约定为 JSON格式如下:
```
{
code: "00000", // 状态码 msg: "操作成功!", // 提示信息 data: {} // 数据 }
```

注:"00000":业务成功状态码;非"00000"都为业务失败。
为了防止服务器端状态码泛滥成灾,code可以为"",这时 msg 里面则是相应的错误信息,只为给用户提示。

导火线

项目开发完毕,测试人员去测试,提如下Bug:

如果用户未登录,进个人中心,提示用户未登录,然后会去登陆view;而在下单页,提示用户未登录,却没有去登陆view。

然后前端童鞋开始去修复该问题,查出如下问题:

  • 个人中心服务器接口返回的数据格式:
    ```
    {
    code: "00008",
    msg: "用户未登录,请登录",
    data: [ ]
    }
    ```
  • 下单页服务器接口返回的数据格式:
    ```
    {
    code: "",
    msg: "用户未登录,请登录!",
    data: [ ]
    }
    ```

然后前端童鞋对服务器端童鞋讲,这里你应该返回给我code: "00008",我这边一看 code便知是用户未登录,就可以做出相应的操作,这里你只返回提示信息,我这边不好做更加细腻的操作。

然后,后端童鞋开始尝试给该地方添加上 code。
开始着手修改代码:
首先找到接口方法里面发现如下 demo:
```php
$order = kernel::single('sysapiecouponorder')->create($params, $msg);
if (!$order) {
return array('code' => '', 'data' => array(), 'msg' => $msg); }
return array('code' => '00000', 'data' => $order);
```
改方法返回array(); 在外部统一入口、出口处再返回 JSON出去。

sysapi_ecoupon_order
```php
public function createNew($params, & $msg)
{
// 获取用户信息 $member_info = app::get('b2c')->model('members')->get_current_member();

if (empty($member_info)) {
    $msg = app::get('ecoupon')->_('用户未登录,请登录!');
    return false;
}
// 继续下面的业务处理

}
```

接口调用的kernel::single('sysapi_ecoupon_order')->create($params, $msg);这里面做实际的业务处理,错误信息是通过 $msg 向上传递出去,外部没办法通过 $msg 获知对应的 code。然后给前端童鞋讲这种情况没办法返回 code给你。

前端就只能通过判断 msg的方式来修复该问题
然后写了如下 demo:
```javascript
if("用户未登录,请登录!" == data.msg) {
// 用户未登录,去登录 // ... }
```
然后提交,测试,通过,上线,N天后

有人跑过来讲:下单页 与 个人中心的提示有点不同,貌似多了个 "!"。(举例而已,更多的可能是提示不友好、错别字等情况)

然后后端同学修改为 $msg = app::get('ecoupon')->_('用户未登录,请登录'); 提交,测试不通过,前端同学再修改为if("用户未登录,请登录" == data.msg),提交,测试通过

// 如此反反复复

终究有一天:产品、测试,前端、后端混战了一场。N人,卒.....

重新正视问题

最终前后端得出结论:要想对用户实现更加友好的体验,前后端数据必须有个标识具有唯一性不变性。而现在用的 msg却不具备,还是得用 code。并且这里前后端极度耦合msg。

后端童鞋回来继续修改代码,开始着手给这里添加上相应的 code。
开始思考该怎么添加 code,现在的问题是 create( ) 方法可能是其他童鞋开发,内部返回的提示信息,我这边是调用者,不能确定方法内部到底会返回什么提示信息,无解。

忽然,有一天想到,我在调用该方法之前检查下用户有没有登录就OK了,然后开始写如下实现:

public function create($params)
{
    $member = app::get('b2c')->model('members')->get_current_member();

    // 登录验证
    if (empty($member)) {
        return array('code' => '00008');
    }

    $msg = '';

    $order = kernel::single('sysapi_ecoupon_order')->create($params, $msg);
    if (!$order) {
        return array('code' => '', 'data' => array(), 'msg' => $msg);
    }
    return array('code' => '00000', 'data' => $order);
}

呵呵,好机智的少年。
然后告诉前端,这里可以返回 code了,前端愉快的删掉原来那坨判断 msg的代码,而在 ajax请求的地方统一判断 code就能预知用户未登录,做出相应的操作。

经测试,上线。一切又回到了美好时光。

随着时光的流逝,业务的增加,后端童靴发现Order类里面如下 demo:
```php
public function create($params)
{
$member = app::get('b2c')->model('members')->getcurrentmember();

// 登录验证
if (empty($member)) {
    return array('code' => '00008');
}

// 实际业务处理....

}

public function getOrderList($params)
{
$member = app::get('b2c')->model('members')->getcurrentmember();

// 登录验证
if (empty($member)) {
    return array('code' => '00008');
}

// 实际业务处理....

}

public function getOrderDetail($params)
{
$member = app::get('b2c')->model('members')->getcurrentmember();

// 登录验证
if (empty($member)) {
    return array('code' => '00008');
}

// 实际业务处理....

}

// ...
```

这都是什么玩意............ 然后开始封装,稍微好了点

又过了一段时间,有人过来说创建订单还需要优化体验,
点击创建订单提示如下:

  • 超过最大购买量——给出提示,继续留在创建订单页
  • 该商品已卖光或已下架——引导用户去商品列表页

这时,前端童鞋告诉后端童鞋,商品下架的时候,你也应该返回一个状态码。

后端童鞋开始打算添加 code,发现如下 demo
```php
kernel::single('sysapiecouponorder')->create($params, $msg);
```

这里的提示信息是 $msg 返回的,用户登录外部可以提前检测,这里的商品能否购买要实现添加 code也需要提前检测,将来要是需要添加类是功能岂不是...... 每需要一个精确的 code返回出去,这里就需要添加检测,这里代码将会变得无法直视。
况且这里本该在业务里面检测,一切不那么友好起来了。

再次思考,代码写的不爽了,一定是哪里不对

问题所在

开始怀疑 public function create($params, & $msg) { } 这里不应该是通过 & $msg 来作为 调用者与 被调用者之间的 错误信息通信约定,一切的问题都出在了这里。错误消息向上传播的约定不合适

如果这里约定的是 code作为错误向上传播一切的问题即将不复存在。在调用业务方法之前的检测代码就都可以去掉了,代码简约,一切又美好起来。

接下来继续思考,使用 code作为业务处理失败消息传递问题又来了

  • 现在已有的业务代码都是如此定义public function create($params, & $msg),怎样更加友好的替换成 code
  • 如果使用 code,code只能服务器端 与 前端约定的一个具有唯一性的标识(code 比 msg 对国际化的实现更加容易)但是并不能直接展示给用户,那么就需要定义每个 code 的代表的意义 与 对应的提示信息。那么问题来了,code 应该已怎样规范来定义所代表的含义

再来看如下常用的两种方法定义:

  1. public function create($params, & $msg)
  2. public function create($goodsId, $num, & $msg)

第一种方式,参数通过一个 $params数组传递过来,方法内部在把错误提示放到 $msg中。

  • 好处:$params是个数组,里面参数可以任意添加
  • 缺点:该方法调用者在外部不能知道该方法需要什么参数,必须来看该方法内部实现,做出对应的数组 key的转换(如: user_id 转 userId)。方法调用者 与 方法实现 极度耦合。维护成本大、出Bug系数高

第二种方式,按基本类型分别传递单个参数

  • 好处:方法调用者根据方法定义就能够知道方法具体需要的参数,调用方法时不需要作 key转换,只需要传递对应的参数即可
  • 缺点:参数数目过多时,惨不忍睹

这里有如下问题:

  • 这里如何已一种更加容易维护,扩展的方式来处理(Java里面方法参数已对象的方式传递可以借鉴
  • 这里的& $msg 真的合适吗,如果是第二种方式定义的方法,以后扩展个 $phone 该如何处理?public function create($goodsId, $num, & $msg, $phone='')这样么?怎么看怎么蛋疼
  • 再来看不通过 & $msg传递错误信息之后的代码
    ```php
    public function create($goodsId, $num)
    {
    if ( ? ) {
    // 返回状态码 return '0001'; }
    if ( ? ) {
    // 返回状态码 return '0002'; }
    // 创建订单
    // ...
    // 返回订单信息
    return $order;
    }
    ```
    看似实现了,但是方法调用者,怎么调用怎么蛋疼,一会返回状态码,一会返回订单信息,完全两种类型。

综合以上问题:

得出以下结论:

  • 业务处理失败消息要以 code 的方式向上传递给调用者
  • 业务处理失败消息以参数的方式传递不是很适合,并且不能以 return的方式返回

再次思考,最终从 Java里面想到了一点思路(幸好是 Java出身。疑问:为何面试的时候 Java的工作经验都不算在 PHP工作经验里呢,并没有因此而加分)

解决方案:

  • 自定义一个异常类,包括 codo属性 和 msg 属性
  • 凡是遇到业务不能正常处理的时候就创建一个异常对象,设置对应的 code 或者 msg属性(为了减少 code泛滥,这里的 code 与 msg 可以2选一,如果前端需要做精准的处理,就设置 code,如果只是为了给用户提示,就只返回 msg,则可以减少一个 code),然后抛出异常
  • 方法调用者在外部统一捕捉该异常,如 接口的统一入口出口的方法内部处理

因个人工作时间、项目经历不多、归根结底经验不足。现在将该方案写下来,还望有经验的大神拍砖,以免给团队挖坑,以上 $msg 就是 N久以前埋下的坑。

该文章发布在自己站点:http://www.webdevs.cn/article/91.html

等待第一条回复
登录后回复,如无账号,请使用邀请码注册