http+json 格式的接口开发实践与思考
业务背景
最近一段时间,都在做接口对接,项目也差不多上线了,正好也总结下自己的想法和思考。
项目的主要目的是给我公司店铺的店长、员工、和业务品牌领导提供一个入口,可以很方便直观地看到店铺之间,员工之间的一些排名情况,比如展示某一段时间内品牌内店员销售排行、店员当月kpi数据、店铺所有店员单月销售排行、还有一些PK接口(店铺pk和员工pk)pk维度目前包括如下。
销售金额
销售订单数
客单价
附加值/连带率
VIP会员数
普通会员注册数
指定款号+色号销售
指定销售单价+1分
指定款号销售
指定SKU规则销售积分
普通会员注册数,销售额大于0
每个店铺销售TOP名次列表
会员入会率
正价销售额度PK积分
区域开卡销售额PK积分
复购率
留存率
说到底我的工作就是根据用户的请求参数,根据入参判断做一些逻辑的判断,分别调用不同的业务,返回对应的json数据给前端(这里指企业微信公众号)展示。
一般做法
上面大致了解了大致的业务需求,下面开始剖析接口设计的方法,一般情况下接口开发的流程大致如下所示:
上面这种逻辑应付下面两种类型的接口开发是没问题的
1. 品牌内店员销售排行
2. 业绩查询接口
因为这两种类型的接口都比较单一,客户端可以把所有的参数发送到后台即可,比如
http://host/shoppk/PersonalCompass.action?data={“shopNumber”:”1072”,”clerkNumber”:”1072_018”,”year”:”2017”,”month”:”1”}&actType=list 表示店员当月每天销售数据
这种方式的好处:简单并且方便
但是对于上面的pk接口来说,就会存在以下问题:
1. 大量代码重复
上面pk接口已经列举了17种不同的pk维度,员工、店铺之间的pk 2*17 = 34 这样就需要提供34个接口,如果按照上面这种形式来开发的话,起码要提供34个接口,这里可能就会存在很多重复性的代码。
2.http请求参数过多,占用网络资源,传输速度慢
比如以店铺之间进行销售金额pk,那么按照传统方法的话,http请求中需要穿 开始时间、结束时间、参与pk的店铺编号shopNumber
(”3602”,”3604”,”3607”,”3608”,”3609”,”3610”,”3611”,”3613”) ;如果pk的店铺过多的话,这个shopNumber 可能会过长。 如果这些参数全部通过http传输,肯定会有效率问题
3.扩展性不好
起初pk的维度并没有这么多,起初可能就前面几个常用的维度,随着各品牌事业部对于店铺、店员的考核需求不同,后面会陆续增加其它的维度,如果按照传统设计,那么新增一个维度pk就需要开发一个新接口。
4.不够灵活
对于前端来讲,需要根据不同的pk维度传输不同的参数,这其实也是一个不好的设计,客户端的职责过多,对于前端界面设计也不美观。
改进做法
如上面所列举的一些缺点,很明显pk接口明显不能用一般方法的去设计开发。其实仔细分析,我们可以发现一些共同点,都是pk业务,不同的地方是pk的类型(店铺、员工),和pk的时间范围,还有pk的关联sku等。
其实可以把每个pk请求表示一个活动,不同pk业务参数可以放在活动关联的配置详情表,活动关联的sku表中。以后调用pk接口的时候,只需要提供一个统一的pk接口,不同的pk维度传输不同的activityId,后台拿到activityId后,可以判断activityId的类型,是店铺还是员工? 还可以查询activityId关联的的详情(开始日期,介绍日期,哪些sku参与pk,哪些店铺参与pk)。改进后的接口流程图如下所示:
业务层的逻辑判断:
下面以复购率,留存率这两个pk维度来讲解下我这边设计的开发工作。
下面是几个数据库表的设计
1 数据库设计
活动配置表:
CREATE TABLE
tb_activity_config
(
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
activity_id INT NOT NULL COMMENT '活动ID',
pk_unit INT NOT NULL COMMENT 'PK单位(1:店铺之间PK 2:员工之间PK)',
pk_type INT NOT NULL COMMENT 'PK类型',
pk_dimension INT NOT NULL COMMENT 'PK维度',
range_region INT COMMENT '参与活动区域',
range_city VARCHAR(30) COMMENT '参与活动的城市',
ranking_top INT DEFAULT '1500' COMMENT '排行榜显示名次',
show_top INT DEFAULT '100' COMMENT '显示排名数',
sale_top TINYINT DEFAULT '3' COMMENT '销售显示的TOP名额',
sale_amount DECIMAL(10,2) COMMENT '销售单金额',
sale_number INT DEFAULT '0' COMMENT '销售件数',
sale_discount DOUBLE DEFAULT '0.895' COMMENT '正价销售折扣',
sale_denominator DECIMAL(10,2) DEFAULT '1200.00' COMMENT '销售基数分母',
is_nickname TINYINT(3) DEFAULT '1' COMMENT '是否显示昵称(0:不显示 1:显示)',
del_flg TINYINT(3) DEFAULT '0' COMMENT '删除FLG (0:正常 1:已删除)',
create_date INT COMMENT '创建时间',
update_date INT COMMENT '更新时间',
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='店铺活动设置表';
活动详情表:
CREATE TABLE
tb_activity_desc
(
activity_id INT NOT NULL AUTO_INCREMENT COMMENT '活动ID',
activity_name VARCHAR(50) NOT NULL COMMENT '活动名称',
t_brand VARCHAR(10) COMMENT '所属品牌',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME NOT NULL COMMENT '结束时间',
award VARCHAR(100) COMMENT '活动奖品',
picurl VARCHAR(150) NOT NULL COMMENT '图文封面图',
thumb_picurl VARCHAR(150) NOT NULL COMMENT '封面缩略图',
activity_desc text COMMENT '活动描述',
end_flg TINYINT(3) DEFAULT '0' COMMENT '结果公布FLG (0: 未公布 1:已公布)',
result_image VARCHAR(90) COMMENT '结果公布图片',
orderby INT COMMENT '排序号',
del_flg TINYINT(3) DEFAULT '0' COMMENT '删除FLG(0:正常; 1:已删除)',
create_date INT COMMENT '创建时间',
update_date INT COMMENT '更新时间',
PRIMARY KEY (activity_id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='店铺活动表';
活动维度配置表:
CREATE TABLE
tb_activity_dimen
(
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
pk_type INT NOT NULL COMMENT 'PK类型ID(1:销售PK ; 2:CRM PK ; 3:分享PK)',
pk_type_name VARCHAR(10) COMMENT 'PK类型名称',
pk_dimension_id INT NOT NULL COMMENT 'PK维度ID',
pk_dimension VARCHAR(30) NOT NULL COMMENT 'PK维度',
del_flg TINYINT(3) DEFAULT '0' COMMENT '删除FLG (0:正常 1:已删除)',
create_date INT COMMENT '创建时间',
update_date INT COMMENT '更新时间',
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='店铺活动PK维度表';
活动店铺关联表:
CREATE TABLE
tb_activity_shopids
(
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
activity_id INT NOT NULL COMMENT '活动ID',
shop_id VARCHAR(10) NOT NULL COMMENT '店铺ID',
shop_name VARCHAR(50) NOT NULL COMMENT '店铺名称',
create_date DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
INDEX shop_id (shop_id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='活动参与店铺名单表';
活动关联的商品sku表:
CREATE TABLE
tb_activity_sku
(
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
activity_id INT COMMENT '活动ID',
commodity_number VARCHAR(15) COMMENT '货品编号',
product_color_id VARCHAR(4) COMMENT '商品色号',
PRIMARY KEY (id),
INDEX commodity_number (commodity_number, product_color_id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='PK活动商品SKU表';
2 活动配置表造数据
通过活动配置界面, 新增一个员工 复购率pk的活动(包含pk店铺,开始日期、介绍日期等)
3 通过维度得知对应的activity_id
通过sql查询:
// 根据维度查询活动配置
SELECT c.activity_id,d.* FROM tb_activity_config c left join tb_activity_desc d
on c.activity_id = d.activity_id
WHERE pk_unit = 2 and pk_dimension = 116
4 编写业务sql计算复购率
原始复购率表结构
由于计算业务比较复杂,这边就不多做介绍,直接贴上sql,其实就是对一张单表的各种过滤查询
select a.ClerkCode,sum((b.RePurchaseCount -a.RePurchaseCount)*0.5 + (b.RePurchaseRate -a.RePurchaseRate)*0.5) as totalScore from
(SELECT a.ClerkCode,a.RePurchaseCount,a.RePurchaseRate
FROM report_rerurchase a
where 1=1 and a.StatisticsDate =
(SELECT distinct( a.StatisticsDate) FROM report_rerurchase a
where 1=1 and a.StatisticsDate>= 20180101 and a.StatisticsDate<= 20180110
and a.StoreCode in ('3602','3604','3607')
and a.ClerkCode!='' order by 1 asc limit 1 )
and a.StoreCode in ('3602','3604','3607')
and a.ClerkCode!='') a inner join
( SELECT b.ClerkCode,b.RePurchaseCount,b.RePurchaseRate
FROM report_rerurchase b
where 1=1 and b.StatisticsDate =
(SELECT distinct( a.StatisticsDate)
FROM report_rerurchase a
where 1=1 and a.StatisticsDate>= 20180101 and a.StatisticsDate<= 20180110
and a.StoreCode in ('3602','3604','3607')
and a.ClerkCode!=''
order by 1 desc
limit 1
)
and b.StoreCode in ('3602','3604','3607')
and b.ClerkCode!='') b
on a.ClerkCode = b.ClerkCode
group by a.ClerkCode
order by totalScore desc
统计结果如下:
5 编写Java后台业务逻辑
确定sql无误后,可以开始编写Java后台的业务逻辑代码,这里只贴出几个关键的逻辑代码
控制层:
缓存层:
因为从复购率这些数据从crm表中每天凌晨抽取到中间表report_rerurchase中,所以查出来的数据当天是不会变的,这里可以把数据放到本地缓存中 ,等下一次查询的时候,直接从缓存中拿数据。加快查询速度,并且减少数据库服务器的压力。
业务层:
数据层:
/****
* 店员复购率查询
* @param startTime 开始日期
* @param endTime 结束日期
* @param shopIds 店铺ID 集合
* @param rankingTop 排名
* @return
*/
public List<Map<String, Object>> getRePurchaseRateTopListByClerk(String startTime, String endTime,
List<String> shopIds, int rankingTop) {
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
DBFactory dbFactory = new ShoppkCrmTestDBFactory();
DBObject db = null;
try {
db = dbFactory.getDBObjectR();
startTime = startTime.split(" ")[0].replace("-", "");
Integer start = Integer.parseInt(startTime);
endTime = endTime.split(" ")[0].replace("-", "");
Integer end = Integer.parseInt(endTime);
StringBuffer sb = new StringBuffer();
sb.append(" select a.ClerkCode as ClerkCode,sum((b.RePurchaseCount -a.RePurchaseCount)*0.5 + (b.RePurchaseRate -a.RePurchaseRate)*0.5) as totalScore from");
sb.append(" (SELECT a.ClerkCode,a.RePurchaseCount,a.RePurchaseRate ");
sb.append(" FROM report_rerurchase a ");
sb.append(" where 1=1 and a.StatisticsDate= ");
sb.append(" (SELECT distinct( a.StatisticsDate) FROM report_rerurchase a ");
sb.append(" where 1=1 and a.StatisticsDate>=? and a.StatisticsDate<=? ");
sb.append(" and a.StoreCode in ("+SqlKit.parseInQuery(shopIds.size())+")");
sb.append(" and a.ClerkCode!='' order by 1 asc limit 1 ) ");
sb.append(" and a.StoreCode in ("+SqlKit.parseInQuery(shopIds.size())+")");
sb.append(" and a.ClerkCode!='') a inner join ");
sb.append(" (SELECT b.ClerkCode,b.RePurchaseCount,b.RePurchaseRate ");
sb.append(" FROM report_rerurchase b ");
sb.append(" where 1=1 and b.StatisticsDate=");
sb.append(" (SELECT distinct( a.StatisticsDate) FROM report_rerurchase a ");
sb.append(" where 1=1 and a.StatisticsDate>=? and a.StatisticsDate<=? ");
sb.append(" and a.StoreCode in ("+SqlKit.parseInQuery(shopIds.size())+")");
sb.append(" and a.ClerkCode!='' order by 1 desc limit 1 ) ");
sb.append(" and b.StoreCode in ("+SqlKit.parseInQuery(shopIds.size())+")");
sb.append(" and b.ClerkCode!='') b on a.ClerkCode = b.ClerkCode ");
sb.append(" group by a.ClerkCode ");
sb.append(" order by totalScore desc ");
PreparedStatement ps = db.getPreparedStatement(sb.toString());
ps.setInt(1, start);
ps.setInt(2, end);
for (int i = 0; i < shopIds.size(); i++) {
ps.setString(i + 3, shopIds.get(i));
}
for (int i = 0; i < shopIds.size(); i++) {
ps.setString(shopIds.size() + 3 + i, shopIds.get(i));
}
ps.setInt(shopIds.size()*2 + 3, start);
ps.setInt(shopIds.size()*2 + 4, end);
for (int i = 0; i < shopIds.size(); i++) {
ps.setString(shopIds.size()*2 + 5 + i, shopIds.get(i));
}
for (int i = 0; i < shopIds.size(); i++) {
ps.setString(shopIds.size()*3 + 5 + i, shopIds.get(i));
}
ResultSet rs = db.executePstmtQuery();
int top = 1;
while (rs.next()) {
Map<String, Object> map=new HashMap<String, Object>();
map.put("clerkCode", rs.getString("ClerkCode"));
map.put("totalScore", rs.getDouble("totalScore"));
map.put("top", String.valueOf(top));
top++ ;
list.add(map);
}
rs.close();
} catch (Exception e) {
log.error("", e);
} finally {
try {
db.close();
} catch (SQLException sqle) {
log.error("", sqle);
}
}
return list;
}
业务代码ok,启动服务器,发送http请求http://test.applets.teadmin.net:8080/shoppk/ShopPkTopList.action?activityId=314,只需要传输activityId=314参数 即可
响应结果是所有店铺根据复购率大小进行倒序排序.
总结
传统的流程不能满足的话,就需要思考能不能有其他方法解决这个问题,上面的思路是多了一个中间层的概念,加了活动配置表,所有的请求参数都封装在这些配置中,后台只需要拿到activityId 即可。大大简化了前端的工作,也提升了系统的扩展性、灵活性。
更多推荐
所有评论(0)