http+json 格式的接口开发实践与思考

article/2025/10/31 22:02:52

业务背景

最近一段时间,都在做接口对接,项目也差不多上线了,正好也总结下自己的想法和思考。

项目的主要目的是给我公司店铺的店长、员工、和业务品牌领导提供一个入口,可以很方便直观地看到店铺之间,员工之间的一些排名情况,比如展示某一段时间内品牌内店员销售排行、店员当月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 TABLEtb_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 TABLEtb_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 TABLEtb_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 TABLEtb_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 TABLEtb_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 don 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 awhere 1=1 and  a.StatisticsDate>= 20180101  and a.StatisticsDate<= 20180110and 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 awhere 1=1 and  a.StatisticsDate>= 20180101  and a.StatisticsDate<= 20180110and a.StoreCode in ('3602','3604','3607')and a.ClerkCode!=''order by 1 desclimit 1 
)  
and b.StoreCode in ('3602','3604','3607') 
and b.ClerkCode!='') bon 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 即可。大大简化了前端的工作,也提升了系统的扩展性、灵活性。


http://chatgpt.dhexx.cn/article/Z7N2SITi.shtml

相关文章

基于json文件创建后端模拟接口

json-server有很多接口&#xff1b;目的&#xff1a;基于一个json文件就可以创建很多的后端模拟接口&#xff1b; &#xff08;1&#xff09;先创建一个json文件&#xff1a; &#xff08;2&#xff09;然后下载nodejs&#xff1a;下载 | Node.js 中文网 注意&#xff1a;下…

SpringBoot接口接收json参数

1. 创建入参实体&#xff1a; package com.hsm.ls.application.test.domain;/*** Created by lfx on 2018/12/19.*/ public class ActiveRequest {private String sid;private String biz;private String text;public String getSid() {return sid;}public void setSid(String…

JSONP接口

概念&#xff1a;浏览器通过<script>标签的src属性&#xff0c;请求服务器上的数据&#xff0c;同时服务器返回一个函数的调用。这种请求数据的方式叫JSONP 特点&#xff1a;JSONP不属于真正的的Ajax请求&#xff0c;因为它没有使用XMLHttpRequest这个对象 JSONP仅支持G…

java 调用接口为json格式

#simple 直接上代码 ①pom文件导入hutool的war包 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.10</version></dependency>②new JSONObject(); 对象&#xff0c; put你所需要的…

『前端必备』本地数据接口—json-server

文章目录 json-server介绍简介操作步骤 操作数据增&#xff08;POST&#xff09;删&#xff08;DELETE&#xff09;改&#xff08;UPDATE、PATCH&#xff09;查&#xff08;GET&#xff09;筛选分页排序切片(分页)特殊符号添加_gte或_lte获取范围添加_ne以排除值添加_like到过滤…

接口接收JSON数据

1. 使用场景&#xff1a;定义接口供对方调用。请求数据为JSON格式&#xff0c;请求方法为post&#xff0c;请求参数如下&#xff1a; { "num":" 1600330803432", "comCode": "feisu" } 2. 接口接收方法 ResponseBody public Ba…

JSON和API接口初识

首先认识一下JSON JSON官网&#xff1a;http://www.json.org/json-zh.html JSON是一种数据格式&#xff0c;是一种在互联网传输中运用最多的数据交换语言&#xff0c;由于它轻便、灵巧&#xff0c;且能从各种语言中完全独立出来&#xff0c;所以成为目前最理想的数据交换语言…

Java数据接口编写简单例子,Java Json解析,服务端接口输出Json数据,客户端通过HTTP获取接口Json数据

实现效果 实现原因 目前主流的CS结构&#xff0c;数据都是通过RESTful风格接口形式呈现&#xff0c;不管是桌面级应用程序还是手机端&#xff0c;接口是我们获取数据的大多数选择&#xff0c; 主流数据接口呈现形式主要是Json和Xml&#xff0c;后者目前基本渐行渐远。Json的轻量…

json接口(使用,以及自定义)

json使用 在spring boot中&#xff0c;返回一个数组,哈希表&#xff0c;spring boot会自动将List,HashMap转化为json,因此不要自定义转化。 如果自定义转化&#xff0c;后端会爆出一个警告&#xff0c;并且前端出现500&#xff08;404&#xff09;&#xff0c;无法访问。因此…

前后端角度看接口(什么是json)?

文章目录 什么是JSON&#xff1f;为什么需要JSON&#xff1f;总结 什么是JSON&#xff1f; JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集&#xff0c;采用完全独立于编程语言的文本格…

JSON接口

RestController 相当于ControllerResponseBody两个注解的结合&#xff0c;返回json数据&#xff0c;不能返回jsp,html页面。 Autowired 对类成员变量、方法及构造函数进行标注&#xff0c;让 spring 完成 bean 自动装配的工作。 PostMapping 映射一个POST请求。 PutMapping 处理…

JSON端口操作实例

JSON 端口可直接实现在 JSON 和 XML 之间进行转换。端口会自动检测输入文件是 JSON 还是 XML&#xff0c;然后将文件在两种格式间相互转换。 该端口较多的是运用在API接口调用集成方案的项目当中&#xff0c;我们以百思买项目为例&#xff0c;知行之桥将接收到的百思买的EDI报…

JSON-初识+解析

1. JSON 1.1 什么是JSON JSON&#xff08;JavaScript Object Notation, JS 对象标记&#xff09;&#xff1a;是一种轻量级的数据交换格式。JSON使用JavaScript 语法来描述数据对象&#xff0c;采用完全独立于语言的文本格式&#xff0c;易于存储和交换。 JSON 的网络媒体类型…

unity协程coroutine 简明教程

本篇内容基于 https://gamedevbeginner.com/coroutines-in-unity-when-and-how-to-use-them/ 以及官方教程 为什么使用协程 协程非常适合设置需要随时间发生变化的游戏逻辑。很自然我们会想到update&#xff0c;update里指出每一帧unity会执行什么操作。协程则可以将代码从up…

浅析Unity协程实现原理

介绍 协程Coroutine在Unity中一直扮演者重要的角色。可以实现简单的计时器、将耗时的操作拆分成几个步骤分散在每一帧去运行等等&#xff0c;用起来很是方便。 但是&#xff0c;在使用的过程中有没有思考过协程是怎么实现的&#xff1f;为什么可以将一段代码分成几段在不同帧执…

Unity协程和线程的区别深入理解(附实验展示)

Unity协程和线程的区别附实验展示 写在前面协程、进程、线程的概念进程与线程的区别协程与线程的区别实验1&#xff1a;协程中执行普通函数实验2&#xff1a;协程中开启另一个协程实验3&#xff1a;协程中开启WWW请求实验4&#xff1a;一个脚本中多个协程访问临界资源实验5&…

Unity 协程 Unity Task UniTask

协程 使用 StartCoroutine 和 IEnumerator yield return null 暂停执行并随后在下一帧恢复 yield return new WaitForSeconds(1f); 延迟1秒 waitfor系列有好几个 WaitForSeconds 和 WaitForSecondsRealtime 的区别 使用缩放时间将协程执行暂停指定的秒数。 实际暂停时间等于…

Unity 协程探究

一、官方手册中的描述 1、Manual/Coroutines 函数在调用时, “从调用到返回” 都发生在一帧之内&#xff0c;想要处理 “随时间推移进行的事务”&#xff0c; 相比Update&#xff0c;使用协程来执行此类任务会更方便。 协程在创建时&#xff0c;通常是一个 “返回值类型 为 …

Unity 协程底层原理解析

1、协程 unity是单线程设计的游戏引擎&#xff0c;unity实际上有多条渲染线程&#xff0c;但对于unity调用我们编写的游戏脚本&#xff0c;都是放在一个主线程当中进行调度的。因此对于我们写的游戏脚本unity是单线程的。 协程不是进程或者线程&#xff0c;它的执行过程更类似…

Unity协程那些事儿

Unity协程那些事儿 1、什么是协程&#xff1f;2、协程的使用3、关于yield4、关于IEnumerator/IEnumerable5、从IEnumerator/IEnumerable到yield6、Unity协程机制的实现原理7、源码分析8、总结 1、什么是协程&#xff1f; 用过Unity的应该都知道协程&#xff0c;今天就给大家来…