APIJSON复杂业务深入实践(类似12306订票系统)

1 篇文章 0 订阅

一、前言

APIJSON一款自动化的ORM框架,对于前后端开发,带来了很大的便利(有了它,后端不再需要写常规的业务代码,简单的,复杂的,增删改查),为什么这么说,请看下文分析。因为此文针对的是APIJSON深入应用,所以各位看官,如需了解APIJSON基本使用,请先移驾APIJSON官方网站官方文档。

二、文章背景

深入使用APIJSON,是因为接手了一个类似12306火车的订票系统,具体业务场景,我们先来看下ER图票务系统主要业务表
库表备注:

站点:opm_site(每条线路的站点,都是来自这些站点,包含一些定位,名称等)
站点列表:opm_site_list(每条线路包含的所有站点)
线路:opm_line(具体线路)
班次:opm_car_freq(线路下的具体班次,定义了票数、时间等必要信息)

业务说明:类似12306,用户选择出发、到达站、出发时间

在这里插入图片描述
根据用户,选择的具体站点,时间,搜索班次列表

在这里插入图片描述
选择班次,提交订单(订单确认)
在这里插入图片描述

三、常规与复杂的增删改查

常规增删改查,在官方文档语法&示例,已经有基础说明,这里不再赘述。根据以上业务,我们来解析下,其中涉及的复杂业务需求。

1、根据出发与到达站,搜索所有班次
(1)只能查出,包含用户搜索的出发、到达站点的线路下所有班次

解析:我们从线路(opm_line)为分析出发点,那么线路关联的站点列表(opm_site_list)所有站点,必须包含用户搜索的出发与到达站,并且站点顺序必须是出发站在前、到达站在后(因为只是包含,不限制顺序,有可能搜索出来反方向的车次,这点应该好理解),然后根据线路,找班次,出发时间在班次上限制的。

下边我们根据以上需求,先想想sql怎么写呢(在上手APIJSON初期,一直有个不好的依赖,总是对于复杂的需求,一上来就想直接用APIJSON实现,可是自己SQL都没想好能不能实现)

我们梳理下,线路包含出发、到达站,对应班次肯定也包含,那第一步我们先要筛选包含用户出发、到达站的线路。

select * from opm_line ol
inner join opm_site_list osl on osl.opm_line_id = ol.id
where opm_site_list.opm_site_id = 1 //筛选包含某个站点?

将线路表、站点列表关联查询,这好像也不行,因为只能筛选一个站点
怎么能筛选两个站点呢?
那将【站点列表】根据线路id内联一次,不就有线路的两个站点了吗!(如果看官对这里有疑惑,请思考或补充单表内联数据关系)

select startSite.opm_line_id
from opm_site_list startSite 
inner join opm_site_list arrivedSite on startSite.opm_line_id = arrivedSite.opm_line_id
where startSite.opm_site_id = 1 //出发站点id
and arrivedSite.opm_site_id = 2 //到达站点id

根据线路id内联【站点列表】后,再根据出发、到达站点id筛选只包含,出发、到达站点的线路id。
这样第一步包含搜索的站点,没问题了,但是有个问题,这样会搜索出反方向的线路,是不是?
用户只希望搜索自己搜索方向的线路,比如用户搜【北京->上海】,系统包含线路【北京->天津->上海】和【上海->天津->北京】,用户只希望搜出来【北京->天津->上海】,另外一条要过滤,怎么过滤?
别忘记我们【站点列表】有个排序字段,即线路下,每个站点的排序号。在数据录入时,该字段是这样的:
                    opm_site_list站点列表
id     opm_site_id(站点id)   opm_line_id(线路id)   seq(排序)
1           1(北京站点id)        1                            1
2           2(天津站点id)        1                            2
3           3(上海站点id)        1                            3
4           3(上海站点id)        2                            1
5           2(天津站点id)        2                            2
6           1(北京站点id)        2                            3

根据排序字段实现只要startSite.seq<endSite.seq即可,即:
select startSite.opm_line_id
from opm_site_list startSite 
inner join opm_site_list arrivedSite on startSite.opm_line_id = arrivedSite.opm_line_id
where startSite.opm_site_id = 1 //出发站点id
and arrivedSite.opm_site_id = 2 //到达站点id
and startSite.seq<endSite.seq //出发站点的排序号,必须小于到达站点的排序号

有了所有线路,那么班次就很容易查出来了,查出来的班次也就包含了用户输入的出发到达站,如下:
select * from opm_car_freq //筛选班次
where opm_line_id in (
	select id from opm_line  //筛选线路
	where id in (
		select startSite.opm_line_id
		from opm_site_list startSite 
		inner join opm_site_list arrivedSite on startSite.opm_line_id = arrivedSite.opm_line_id
		where startSite.opm_site_id = 1 //出发站点id
		and arrivedSite.opm_site_id = 2 //到达站点id
		and startSite.seq<endSite.seq //出发站点的排序号,必须小于到达站点的排序号
	)
) and depart_time like '%2020-04-09%' //筛选出发时间,当然实际sql可能不是这样,只是示例搜索某一天,因为数据库时间是datetime类型(可能存在多个时间的班次,如2020-04-09 12:00:00、2020-04-09 13:00:00)

现在我们sql的实现思路已经梳理出来了,下边就是APIJSON查询就简单了

APIJSON查询

{
	"sql@": { //子查询:筛选包含的用户输入出发、到达站的所有线路id
         "from": "Opm_site_list",
         "join": "&/Opm_site_list:to/opm_line_id@",
         "Opm_site_list": {
             "@column": "opm_line_id",
             "opm_site_id": 1, //出发站点id
             "@raw": "siteListWhereItem1" //注意:这是原始sql替换关键字,即该关键字,根据内容,在后台查找匹配的内容,直接替换。目前的版本,该字段的内容,在后台配置的(为了实现startSite.seq<endSite.seq条件),具体配置方式见demo中DemoSQLConfig.java的static{}代码块自定义where条件拼接,我的配置是:RAW_MAP.put("siteListWhereItem1","`Opm_site_list`.`seq` < `to`.`seq`");
         },
         "Opm_site_list:to": {
             "opm_line_id@": "/Opm_site_list/opm_line_id",
             "@column": "",
             "opm_site_id": 2 //到达站点id
         }
     },
     "[]": {
         "Opm_car_freq": { //查包含用户搜索的出发、到达站点所有班次
             "opm_line_id{}@": {
                 "from": "Opm_line",
                 "Opm_line": {
                     "id{}@": "sql", //引用上边sql@子查询
                     "@column": "id"
                 }
             },
             "depart_time~": "2020-04-09" //模糊搜索发车时间
         },
         //以下部分可以忽略,主要是前段展示额外的查询
         "Prm_spell_group":{
             "opm_car_freq_code@": "/Opm_car_freq/code",
             "status":3
         },
         "Prm_promotion":{
             "id@": "/Prm_spell_group/prm_promotion_id"
         },
         "Opm_line":{
             "id@": "/Opm_car_freq/opm_line_id"
         },
         "[]":{
             "Opm_site_list":{
                 "opm_line_id@":"[]/Opm_line/id",
                 "@order":"seq+"
             },
             "Opm_site":{
                 "id@":"/Opm_site_list/opm_site_id"
             }
         },
         //查询的班次分页,该分页功能,主要是指对本json层,第一张库表(Opm_car_freq)分页
         "page": 0,
         "count": 20,
         "query": 2
     },
     "total@":"/[]/total"
 }
2、根据用户选择的班次,下单(提交订单)

下单过程一般系统设计都比较复杂,比如涉及到各种条件判断,支付发起(微信、支付宝等),超时缓存等,APIJSON怎么实现呢?目前对特别复杂的场景,要全靠APIJSON已有的自动化接口,很难实现,但是我们可以复用其功能,也能极大的简化许多工作量。

说明:对于目前APIJSON,只支持单一的新增、单一的修改、单一的删除、单一的查询,所谓单一,是指要么是新增,要么删除,自动化接口不能即删除、又新增。(目前笔者正在实现该复合接口)

什么意思呢?这种场景自己写接口,复用APIJSON的一些方法,把APIJSON的事务剥离出来,自己代码控制事务,然后在代码中,根据自己的业务场景,调用APIJSON的增删改查

具体实现方式,我是重写了AbstractParser.parseResponse方法,仅仅把该方法的事务注释了,然后,单独写了,事务开始、提交、回滚、关闭的方法,可以由代码来控制事务。如下:

import apijson.*;
import apijson.server.*;
import apijson.server.JSONRequest;
import com.alibaba.fastjson.JSONObject;
import com.smart.cloud.authentication.server.function.RemoteFunctionInstance;

import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.HashMap;


/**请求解析器
 * @author Lemon
 */
public class ExtendRequestParser extends AbstractParser<Long> {


	public ExtendRequestParser() {
		super();
	}
	public ExtendRequestParser(RequestMethod method) {
		super(method);
	}
	public ExtendRequestParser(RequestMethod method, boolean noVerify) {
		super(method, noVerify);
	}

	@Override
	public Verifier<Long> createVerifier() {
		return new InvalidVerifier();
	}

	@Override
	public MYSQLConfig createSQLConfig() {
		return new MYSQLConfig();
	}

	@Override
	public MYSQLExecutor createSQLExecutor() {
		return new MYSQLExecutor();
	}

	private RemoteFunctionInstance function;

	@Override
	public Object onFunctionParse(JSONObject jsonObject, String fun) throws Exception {
		if (function == null) {
			function = new RemoteFunctionInstance(requestMethod, tag, version);
		}
		return function.invoke(function,jsonObject,fun);
	}

	@Override
	public ObjectParser createObjectParser(JSONObject request, String parentPath, String name, SQLConfig arrayConfig, boolean isSubquery) throws Exception {
		return new RequestObjectParser(request, parentPath, name, arrayConfig, isSubquery) {
			//TODO 删除,onPUTArrayParse改用MySQL函数JSON_ADD, JSON_REMOVE等
		}.setMethod(requestMethod).setParser(this);
	}


	@Override
	public int getMaxQueryCount() {
		return 1000;
	}

	@Override
	public int getMaxObjectCount() {
		return 200;
	}

	/**重写(扩展)解析请求json,获取对应结果(去掉事务控制,事务控制由单独的方法调用,方便外部复用)
	 * @param request
	 * @return requestObject
	 */
	@NotNull
	@Override
	public JSONObject parseResponse(JSONObject request) {
		long startTime = System.currentTimeMillis();
		Log.d(TAG, "parseResponse  startTime = " + startTime
				+ "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\n ");

		requestObject = request;

		verifier = createVerifier().setVisitor(getVisitor());

		if (RequestMethod.isPublicMethod(requestMethod) == false) {
			try {
				if (noVerifyLogin == false) {
					onVerifyLogin();
				}
				if (noVerifyContent == false) {
					onVerifyContent();
				}
			} catch (Exception e) {
				return extendErrorResult(requestObject, e);
			}
		}

		//必须在parseCorrectRequest后面,因为parseCorrectRequest可能会添加 @role
		if (noVerifyRole == false && globleRole == null) {
			try {
				setGlobleRole(RequestRole.get(requestObject.getString(JSONRequest.KEY_ROLE)));
				requestObject.remove(JSONRequest.KEY_ROLE);
			} catch (Exception e) {
				return extendErrorResult(requestObject, e);
			}
		}

		try {
			setGlobleFormat(requestObject.getBoolean(JSONRequest.KEY_FORMAT));
			setGlobleDatabase(requestObject.getString(JSONRequest.KEY_DATABASE));
			setGlobleSchema(requestObject.getString(JSONRequest.KEY_SCHEMA));
			setGlobleExplain(requestObject.getBoolean(JSONRequest.KEY_EXPLAIN));
			setGlobleCache(requestObject.getString(JSONRequest.KEY_CACHE));

			requestObject.remove(JSONRequest.KEY_FORMAT);
			requestObject.remove(JSONRequest.KEY_DATABASE);
			requestObject.remove(JSONRequest.KEY_SCHEMA);
			requestObject.remove(JSONRequest.KEY_EXPLAIN);
			requestObject.remove(JSONRequest.KEY_CACHE);
		} catch (Exception e) {
			return extendErrorResult(requestObject, e);
		}

		final String requestString = JSON.toJSONString(request);//request传进去解析后已经变了


		queryResultMap = new HashMap<String, Object>();

		Exception error = null;
		//sqlExecutor = createSQLExecutor();
		//onBegin();
		try {
			//queryDepth = 0;
			setQueryDepth(0);
			requestObject = onObjectParse(request, null, null, null, false);

			//onCommit();
		}
		catch (Exception e) {
			e.printStackTrace();
			error = e;
			throw new RuntimeException(e.getCause());
			//onRollback();
		}

		requestObject = error == null ? extendSuccessResult(requestObject) : extendErrorResult(requestObject, error);

		JSONObject res = (globleFormat != null && globleFormat) && JSONResponse.isSuccess(requestObject) ? new JSONResponse(requestObject) : requestObject;

		long endTime = System.currentTimeMillis();
		long duration = endTime - startTime;

		if (Log.DEBUG) { //用 | 替代 /,避免 APIJSON ORM,APIAuto 等解析路径错误
			requestObject.put("sql:generate|cache|execute|maxExecute", sqlExecutor.getGeneratedSQLCount() + "|" + sqlExecutor.getCachedSQLCount() + "|" + sqlExecutor.getExecutedSQLCount() + "|" + getMaxSQLCount());
			requestObject.put("depth:count|max", getQueryDepth() + "|" + getMaxQueryDepth());
			requestObject.put("time:start|duration|end", startTime + "|" + duration + "|" + endTime);
		}

		//onClose();

		//会不会导致原来的session = null?		session = null;

		if (Log.DEBUG) {
			Log.d(TAG, "\n\n\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n "
					+ requestMethod + "/parseResponse  request = \n" + requestString + "\n\n");

			Log.d(TAG, "parseResponse  return response = \n" + JSON.toJSONString(requestObject)
					+ "\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \n\n\n");
		}
		Log.d(TAG, "parseResponse  endTime = " + endTime + ";  duration = " + duration
				+ ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n");

		return res;
	}


	public void onBegin() {
		sqlExecutor = createSQLExecutor();
		super.onBegin();
	}

	public void onCommit() {
		super.onCommit();
	}

	public void onRollback() {
		super.onRollback();
	}

	public void onClose() {
		this.close();
		this.sqlExecutor = null;
		if(this.queryResultMap!=null){this.queryResultMap.clear();}
		this.queryResultMap = null;
	}

	public JSONObject extendParseResponse(JSONObject request){
		JSONObject response = this.parseResponse(request);
		this.clear();
		return response;
	}

	private void clear(){
		this.queryResultMap.clear();
		this.queryResultMap = null;
	}

}

具体怎么使用呢?
下边我们来看看【确认订单】接口

		//4、创建订单
		ExtendRequestParser extendRequestParser = new ExtendRequestParser(PUT,true);
		extendRequestParser.onBegin(); //事务开始
		try{
			//4-1、减库存(减班次库存)
			JSONObject updateCarFreq = new JSONObject();
			
			//...一些json业务配置
			
			extendRequestParser.setMethod(PUT);
			//执行业务
			JSONObject updateCarFreqResponse = extendRequestParser.extendParseResponse(updateCarFreq);
			
			//4-2、创建促销信息(如果不存在)
			if(spellGroup!=null){ //首先得订单参与了促销
				if(orderPromotionInfoId==null){ //促销信息还未创建,则新建
					JSONObject createPromotion = new JSONObject();
					
					//...一些json业务配置
					
					extendRequestParser.setMethod(POST);
					//执行业务
					JSONObject createPromotionResponse = extendRequestParser.extendParseResponse(createPromotion);
				}
			
				//减促销(拼团)库存
				JSONObject updateSpellGroup = new JSONObject();
				
				//...一些json业务配置
				
				extendRequestParser.setMethod(PUT);
				JSONObject updateSpellGroupResponse = extendRequestParser.extendParseResponse(updateSpellGroup);
				
			}

			//4-3、创建订单
			Integer orderId;
			
			JSONObject createOrder = new JSONObject();
			
			//...一些json业务配置
			
			extendRequestParser.setMethod(POST);
			JSONObject createOrderResponse = extendRequestParser.extendParseResponse(createOrder);
			
			//4-4、创建订单班次(如果不存在)
			if(!isExistOrderCarFreq){
				JSONObject createOrderCarFreq = new JSONObject();

				//...一些json业务配置

				extendRequestParser.setMethod(POST);
				JSONObject createOrderCarFreqResponse = extendRequestParser.extendParseResponse(createOrderCarFreq);
				
			}
			
			//4-5、创建订单站点
			JSONObject createOrderSite = new JSONObject();
			
			//...一些json业务配置
			
			extendRequestParser.setMethod(POST);
			JSONObject createOrderSiteResponse = extendRequestParser.extendParseResponse(createOrderSite);
			

			//4-6、创建订单乘客信息
			JSONObject createPassengerInfo = new JSONObject();
			
				//...一些json业务配置
			
			extendRequestParser.setMethod(POST);
			JSONObject createPassengerInfoResponse = extendRequestParser.extendParseResponse(createPassengerInfo);
			

			//4-7、支付下单(第三方支付)(这不是本文的重点,忽略)
			PayOrderVo payOrderVo = new PayOrderVo();
			//.....
			Map<String,String> unifiedOrderResult= unifiedOrder(payOrderVo);

			//4-8、缓存支付签名(这不是本文的重点,忽略)
			SignUtil signUtil = new SignUtil();
			Map<String,String> paramMap = new HashMap<>();
			//......
			System.out.println("签名字符串==="+paySign);
			paramMap.put("paySign",paySign);
			long expireTime = System.currentTimeMillis()+payTimeOut;
			paramMap.put("expireTime",String.valueOf(expireTime));
			//redis缓存
			redisCache.set("order:"+orderId,JSON.toJSONString(paramMap));

			//4-9、添加定时任务执行,订单支付超时处理
			CronTask.addOrderTask(orderId,expireTime);

			extendRequestParser.onCommit(); //事务提交

			return Result.createSuccessResult(orderId);
		} catch (Exception e){
			e.printStackTrace();
			extendRequestParser.onRollback(); //事务回滚
			System.out.println("=======================订单创建失败,执行回滚!==================");
			throw new RuntimeException("订单创建失败,服务器内部错误!");
		} finally {
			extendRequestParser.onClose(); //连接关闭
		}
3、批量新增、修改、删除

该部分作者已更新了文档,更多见APIJSON操作方法

(1)批量删除

接口:delete

请求

{
  "Moment": {
    "id{}": [1,2],
  "tag": "Moment"
}
(2)批量新增

接口:post

请求

{
  "Moment[]": [{
    "id": 1, //一般后台自动生成id,无需请求传
    "content": "这是测试内容"
    },{
    "id": 2,//一般后台自动生成id,无需请求传
    "content": "这是测试内容"
    }],
  "tag": "Moment[]"
}
(3)批量修改

接口:put

请求

{
  "Moment[]": [{
    "id": 1, 
    "content": "这是测试内容"
    },{
    "id": 2,
    "content": "这是测试内容"
    }],
  "tag": "Moment[]"
}
4、更多的可在线测试的例子

APIAuto
无法使用,试试APIAuto8000
对了,测试的账号有:13000082001,密码:123456

未完待续…

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值