元旦三天假期,实现一个电商退单管理系统【三】
一、服务端接口实现
服务端接口用于接收客户端登录、快递公司查询、同步订单、查询已同步订单等业务。主要用tp3.2完成。只写了一个controller,数据库查询都写在controller里了。
tp本来的设计思想,也不分业务逻辑层和数据层。他把数据层和model混合在一起,业务逻辑层和controller合在一起。对于小型项目,快速实现,变更快速响应有很大优势。我这边业务逻辑不是很多,只用了一个入口。
入口处,先放接收一个方法名,根据方法名,再去执行相应的业务逻辑。
接口模板拿了一个以前做手机app的文档改了下,设计如下:
入口函数
1. 扩展了一个简单的枚举值作为与客户端约定的状态码。PHP处理这个有点麻烦。msg不知道怎么对应,还是直接在各处写汉字。
2. 简单的通用返回体getRepsonseMessage结构与客户端保持一致。
3. 签名算法根据流水和入参做了字典排序,然后再加密,与客户端做对比。
4. 只有入口函数是public,其他全部是private。
5. 除几个公共入参外,其他入参都由自己的子函数接收。大部分用php的I方法,订单同步因为要传json字符串,所以用了$_POST。
public function interface(){ $transid = I("transid"); $ts = I("ts"); $sign = I("sign"); $method = I("method"); $this->logger("参数:$transid,$ts,$sign,$method"); if(!$sign || !$transid || !$ts || !$method){ $this->getResponseMessage(ResponseCode::EMPTY_PARAM,"参数为空",""); }else{ if($this->checkSign($sign) || $method == "upgrade"){ switch($method){ case "upgrade": //版本检测 $this->upgrade(); break; case "login": //登录 $this->login(I("uname"),I("ucode")); break; case 'changepwd': //密码修改 $this->changepwd(); case "scan": //扫码 $this->scan(); break; case "syncorders": //同步 $this->syncorders(); break; case "userinfo": //用户中心数据源 $this->userinfo(); break; case "express": //快递公司清单 $this->express(); break; case "express_byid": //根据id查找快递公司 $this->express_byid(); break; case "express_daycount": //今日快递公司统计 $this->express_daycount(); break; case "backorder_daylist": //今日退单列表 $this->backorder_daylist(); break; case 'getprebackorder_bybarcode': //根据扫码单号获取订单详情 $this->getprebackorder_bybarcode(); break; case 'getprebackorder_byid': //根据id获取订单 $this->getprebackorder_byid(); break; case 'preorder_unusal_deal': //异常订单处理 $this->preorder_unusal_deal(); break; case 'backorder_forcheck': //待拆包订单 $this->backorder_forcheck(); break; case 'backorder_forcheck_submit': //提交拆包检查结果 $this->backorder_forcheck_submit(); break; default: $this->getResponseMessage(ResponseCode::UNKNOWN_METHOD,"未知的方法名!",""); break; } }else{ $this->getResponseMessage(ResponseCode::SIGN_ERROR,"签名检验失败",""); } } }
返回体:
private function getResponseMessage($code,$msg,$data="",$mark1=0,$mark2=""){ return $this->ajaxReturn(array("code"=>$code,"msg"=>$msg,"data"=>$data,"mark1"=>$mark1,"mark2"=>$mark2)); }
以登录方法为例:
/** * method: login */ private function login($uname,$ucode){ $user = M("User")->where(array("user_code"=>$uname))->find(); if(!$user){ $this->getResponseMessage(ResponseCode::INVALID_USER,"错误的用户名或密码",null); }else{ //客户端应该上传md5以后的密码 if($user["password"]==$ucode){ $_SESSION["user"] = $user; //更新登录次数和最后登录时间 $user["logincount"] = $user["logincount"]+1; M("User")->save($user); $this->getResponseMessage(ResponseCode::SUCCESS,"登录成功",$user); }else{ $this->getResponseMessage(ResponseCode::INVALID_USER,"错误的用户名或密码",null); } } }
二、后台服务实现
后台服务主要功能为:
1. 超过一定时间订单转为超时订单。
2. 预退货单匹配入库单转拆包。
3. 退货单如果没有预退货单,自动转拆包(需求变更)。
4. 丢包单匹配入库单转拆包。
库表结构比较简单,需求产生过变化,一开始入库单是不转自动拆包的,后来客户提取此需求,我没有扩展再建新表了,就在预退货单表里加了一个order_from字段,1是后台录入预退货,2是入库单没有匹配到预退货单自动拆包,如下图:
考虑到需要常驻后台进程,执行定时任务,服务器为centos,使用springboot搭建,mvc结构,持久层用了jdbcTemplate,表关系比较简单,没有用事务。JdbcTemplate为了获取刚插入的自增键ID,费了老鼻子劲。
dao层实现主要几个功能如下:
超时订单,为了后台修改超时天数能实时生效,每次执行定时任务,查询一下超时天数:
/** * 超时订单 */ public void updateExpiredPrebackorder(){ String sql = "SELECT config_value FROM tb_config WHERE config_code='expire_time'"; int days = this.jdbcTemplate.queryForObject(sql,int.class); log.info("超时设置为:"+days); sql = "SELECT * FROM tb_prebackorder WHERE prebackorder_status=1 AND DATEDIFF(now(),prebackorder_date)>=?"; List<PrebackorderEntity> list = this.jdbcTemplate.query(sql,new BeanPropertyRowMapper(PrebackorderEntity.class),days); for (PrebackorderEntity pbo: list) { sql = "UPDATE tb_prebackorder SET prebackorder_status=5 WHERE prebackorder_id=?"; this.jdbcTemplate.update(sql,pbo.getPrebackorder_id()); //写日志 sql = "INSERT INTO tb_flows(flows_date,flows_man,flows_content,flow_forms,flow_forms_id) VALUES(now(),?,?,?,?)"; log.info("设置为超时:"+pbo.getPrebackorder_id()); jdbcTemplate.update(sql,"系统服务","超过"+days+"天未匹配退货单,自动设置为超时","prebackorder",pbo.getPrebackorder_id()); } }
丢包单匹配入库单:
/** * 丢包单匹配入库单进程 * 考虑到所有的backorder都会流转到prebackorder,所有只要与prebackorder进行匹配即可 * 只有prebackstatus=2(拆包)的才能进行匹配,其他状态不能匹配,如手工录入预退货单、超时单等都不能进行匹配,2是从扫码入库单过来的,才能保证是入库的。 */ public void lostOrdersMatchs(){ String sql = "SELECT a.lostorder_id,b.prebackorder_id FROM tb_lostorder a INNER JOIN tb_prebackorder b ON b.prebackorder_code=a.lostorder_code " + "WHERE backstore_flag=0 AND prebackorder_status=2"; List<Map<String,Object>> list = this.jdbcTemplate.queryForList(sql); log.info("进行一次丢包单匹配,共匹配到"+list.size()+"条入库单"); for (Map<String,Object> map: list) { //1. 更新lostorder的backstore_flag为1 String sql1 = "UPDATE tb_lostorder SET backstore_flag=1 WHERE lostorder_id="+map.get("lostorder_id"); //2. 更新prebackorder的状态为2(拆包检验) String sql2 = "UPDATE tb_prebackorder SET prebackorder_status=2 WHERE prebackorder_id="+map.get("prebackorder_id"); //3. 插入一条流程记录 String sql3 = "INSERT INTO tb_flows(flows_date,flows_man,flows_content,flow_forms,flow_forms_id) " + "VALUES(now(),'系统服务','丢包单入库转拆包','prebackorder',"+map.get("prebackorder_id")+")"; log.info("更新入库单"+map.get("prebackorder_id")+"进入拆包流程"); this.jdbcTemplate.batchUpdate(sql1,sql2,sql3); } }
退货入库单(即快递扫码入库),如果没有预退货单匹配成功,自动进拆包检验流程:
/** * 将超过一定时间的退货单,自动流转进拆包检验 */ public void setBackorder2Prebackorder(){ String sql = "SELECT * FROM tb_backorder WHERE match_preorder_flag=0"; List<BackorderEntity> list = this.jdbcTemplate.query(sql,new BeanPropertyRowMapper(BackorderEntity.class)); for (BackorderEntity bo: list) { //先要判断,如果已存在预退货单号,则不能再次插入 List<PrebackorderEntity> pbolist = this.jdbcTemplate.query("SELECT * FROM tb_prebackorder WHERE prebackorder_code=?",new BeanPropertyRowMapper(PrebackorderEntity.class), bo.getBackorder_code()); if(pbolist!=null && pbolist.size()>0){ log.info("匹配一条,但单号重复,不用进prebackorder,("+bo.getBackorder_code()+"),直接将该订单matc_preorder_flag改为1。"); this.jdbcTemplate.update("UPDATE tb_backorder SET match_preorder_flag=1 WHERE backorder_id=?",bo.getBackorder_id()); }else{ sql = "INSERT INTO tb_prebackorder (prebackorder_code,prebackorder_date,express_id,userid,prebackorder_status,add_date,match_flag,match_date,match_code,prebackorder_from)" + " VALUES (?,?,?,?,?,?,?,?,?,?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); //直接进入状态2,退货单匹配成功,待拆包 PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sql,new int[]{ Types.VARCHAR, Types.DATE,Types.INTEGER,Types.INTEGER,Types.INTEGER,Types.DATE,Types.INTEGER,Types.DATE,Types.VARCHAR,Types.INTEGER }); pscf.setReturnGeneratedKeys(true); Object[] obj = new Object[]{ bo.getBackorder_code(),bo.getBackorder_date(),bo.getExpress_id(),bo.getUserid(),2,new Date(),1,new Date(),bo.getBackorder_code(),2 }; PreparedStatementCreator psc = pscf.newPreparedStatementCreator(obj); int result = this.jdbcTemplate.update(psc,keyHolder); //加一行flows int preid = keyHolder.getKey().intValue(); sql = "INSERT INTO tb_flows(flows_date,flows_man,flows_content,flow_forms,flow_forms_id) VALUES(now(),?,?,?,?)"; jdbcTemplate.update(sql,"系统服务","退货单自动流转","prebackorder",preid); //更新入库单为已匹配 this.jdbcTemplate.update("UPDATE tb_backorder SET match_preorder_flag=1 WHERE backorder_id=?",bo.getBackorder_id()); log.info("自动流转进入拆包订单"); } } }
预退货单匹配入库单:
/** * 匹配预退货订单与退货入库单 */ public void MatchPreOrders(){ String sql = "SELECT * FROM tb_prebackorder WHERE prebackorder_status IN (1,5)"; //超时订单也可以匹配重新入库 List<PrebackorderEntity> list = this.jdbcTemplate.query(sql,new BeanPropertyRowMapper(PrebackorderEntity.class)); for (PrebackorderEntity pbo: list) { String msql = "SELECT * FROM tb_backorder WHERE backorder_code=? LIMIT 1"; List<BackorderEntity> bolist = this.jdbcTemplate.query(msql,new BeanPropertyRowMapper(BackorderEntity.class), pbo.getPrebackorder_code()); if(bolist!=null && bolist.size()>0){ matchOrders(pbo, bolist.get(0)); sql = "INSERT INTO tb_flows(flows_date,flows_man,flows_content,flow_forms,flow_forms_id) VALUES(now(),?,?,?,?)"; jdbcTemplate.update(sql,"系统服务","自动匹配退货单","prebackorder",pbo.getPrebackorder_id()); //再更新下退货单表为匹配成功 jdbcTemplate.update("UPDATE tb_backorder SET match_preorder_flag=1 WHERE backorder_id=?",bolist.get(0).getBackorder_id()); log.info("匹配成功一条记录("+pbo.getPrebackorder_code()+")"); }else{ //有的退单,是在原单号上加一个前缀,所以如果预退单号的indexof入库单号大于0,则匹配成功 sql = "SELECT * FROM tb_backorder WHERE LOCATE(backorder_code,'"+pbo.getPrebackorder_code()+"')>1 LIMIT 1"; bolist = this.jdbcTemplate.query(sql,new BeanPropertyRowMapper(BackorderEntity.class)); if(bolist!=null && bolist.size()>0){ matchOrders(pbo, bolist.get(0)); log.info("匹配成功一条记录("+pbo.getPrebackorder_code()+")"); //写日志 sql = "INSERT INTO tb_flows(flows_date,flows_man,flows_content,flow_forms,flow_forms_id) VALUES(now(),?,?,?,?)"; jdbcTemplate.update(sql,"系统服务","自动匹配退货单","prebackorder",pbo.getPrebackorder_id()); //同步更新退货单表为匹配成功 jdbcTemplate.update("UPDATE tb_backorder SET match_preorder_flag=1 WHERE backorder_id=?",bolist.get(0).getBackorder_id()); log.info("模糊匹配成功一条记录"); } } } }
service层,没有具体的业务逻辑,略。
定时任务:
/** * 每10秒钟执行一次 */ @Scheduled(cron = "0/10 * * * * *" ) public void Match(){ this.service.MatchPreOrders(); log.info("执行一次匹配退单流程"); } /** * 每天0点执行一次 */ @Scheduled(cron = "0 0 0 * * ?") public void updateExpiredPrebackorder(){ log.info("更新一次超时记录"); this.service.updateExpiredPrebackorder(); } /** * 每30秒钟执行一次 */ @Scheduled(cron = "0/30 * * * * ?") public void setBackorder2Prebackorder(){ log.info("执行一次:退货单自动流转至拆包"); this.service.setBackorder2Prebackorder(); log.info("执行一次:丢包单自动匹配"); this.service.lostOrdersMatchs(); }