写在前面:
上周在工作中遇到了一个长SQL实现的报表,因为看到里面涉及到的sql语法,函数有几个常用的,有了不少新的粉丝,感谢感谢.下面时记录一次企业审批的过程,按照自定义的订单审批为例子,感觉可以覆盖大部分自定义审批模板,希望能给大家带来一些帮助,特别是项目准备要用企业审批的,能减少一些弯路.下面设计慕名,代码相关的,均为自定义,需要自己进行适当修改后方可食用.闲话不说,进入正文.
本文旨在给大家提供思路,并且是经过验证的思路,设计项目代码部分,会进行简单修改.编码不符合大众编码习惯的部分,还请大家见谅.
官网文档,开发中遇到问题,先在官网寻找答案
一、预处理
主要是发起审批前的一些校验工作,这个需要根据实际业务需求进行校验.然后记得将审批号与状态更新到订单
@Override
@Transactional(rollbackFor = Exception.class)
public boolean orderConfirm(OrderVO orderVO) {
OrderVO order = orderService.getDetailById(orderVO.getId());
// TODO 判断是否是新建状态 或者审核驳回状态
// TODO 判断是否有仓库 经销商下单没有仓库 会导致后续销售出库单无法查询出来
// TODO 判断是否有有结算方式 经销商下单没有结算方式
// TODO 获取订单产品列表
List<OrderProd> orderProdList = orderProdService.list(Wrappers.<OrderProd>lambdaQuery()
.eq(OrderProd::getOrderId, order.getId()));
for (OrderProd orderProd : orderProdList) {
// TODO 判断可用库存是否充足
// TODO 锁定库存
// TODO 可以使用创建一个锁定库存表,插入数据的方式来锁定库存
}
// 1. TODO 更新客户余额 此处相当于预扣款
// 推送企业微信审核:配置存在,且允许推送企业微信
if (Func.isNotEmpty(wechatProperties.getEnable()) && wechatProperties.getEnable()){
//推送企业微信
String spNo = bizWeChatApply.orderApply(order);
//获取微信审批号,用于后面回调,查看审批状态:通过,驳回等
order.setWxSpNo(spNo);
// TODO 根据余额,做催款提醒
}
//设置为已提交,审核的状态为1
order.setOrderStatus(IntPool.ONE);
//更新审批状态与审批号
return orderService.updateById(order);
}
二、提交审批参数实体类
因为审批参数比较多,所以使用一个Java对象进行封装,同时对于一些默认的基本不会改的可以进行初始化,类似于约定大于配置吧.当然所有参数除了与订单相关的,都进行配置文件配置也是一种选择. 个人觉得配置太多可能会使得配置文件臃肿.取决于具体业务.如果发现默认的配置后期需要改动再重新放打扫配置文件也不是不可以.
/**
* 企业微信提交审批参数实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WechatCommitParam implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 申请人注册手机号,此审批申请将以此员工身份提交,申请人需在应用可见范围内
*/
private String phone;
/**
* 模板的唯一标识id。当前用来标识订单模板.同是订单审批,也可以根据环境等参数使用不同模板
*/
private String templateId;
/**
* 审批人模式:0-通过接口指定审批人、抄送人(此时approver、notifyer等参数可用); 1-使用此模板在管理后台设置的审批流程,支持条件审批。
*/
private Integer useTemplateApprover = 1;
/**
* 抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
*/
private Integer notifyType = 2;
/**
* 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
*/
private String[] summaryInfo;
/**
* 审批申请数据,可定义审批申请中各个控件的值,其中必填项必须有值,选填项可为空,数据结构同“获取审批申请详情”接口返回值中同名参数“apply_data”
* 两个文本
*/
private String[] applyData;
/**
* 附件
* mediaId 获取接口 WeChatApproveUtil.getTempMediaId()
*/
private List<WechatAttachment> mediaFileList;
}
三、审批方法之参数准备
这里主要是对参数进行格式化.按照企业微信官方文档以及实际业务对数据进行封装.这里只是对基本类型进行封装,对于表格数据还是放到调用者哪里
/**
* 订单审批推送
* @param orderVO 订单信息,包含基础信息,顶打死你产品列表等
* @return java.lang.String 审批号
*/
@Override
public String orderApply(OrderVO orderVO) {
// 企微提交审批参数
WechatCommitParam param = new WechatCommitParam();
// 用于确定发起人
param.setPhone(this.getUserPhone(orderVO.getSalesmanId()));
// 通过环境与参数,比如产品列表个数,开发环境还是生产环境等,该ID可以配置在配置文件
param.setTemplateId(template_order_prod);
// 摘要信息,就是收到推送信息后,看到的简要信息
String[] summaryInfo = new String[3];
summaryInfo[0] = "订单审批";
summaryInfo[1] = orderVO.getOrderNo();
summaryInfo[2] = "客户: " + orderVO.getCustomerName();
param.setSummaryInfo(summaryInfo);
// 内容 订单编号 客户名称 收货地址 备注
String[] applyData = new String[7];
applyData[0] = orderVO.getOrderNo();
applyData[1] = Func.isEmpty(orderVO.getStoreName()) ? "无" : orderVO.getStoreName();
applyData[2] = orderVO.getCustomerName();
applyData[3] = orderVO.getReceiverName();
applyData[4] = orderVO.getReceiverPhone();
applyData[5] = orderVO.getReceiverInfo();
applyData[6] = orderVO.getRemark();
param.setApplyData(applyData);
return weChatApproveUtil.commitApproveApplyForOrder(param, orderVO.getOrderProdList(), orderVO.getOrderAuxList());
}
四、审批方法
将API的基本参数设置好,对于表格类型,单独写方法进行封装,这主要是为了后期维护方便.该解耦的方法一定要解耦.个人曾经就看到有上千行代码的方法,当时是一阵头大.
/**
* 订单提交审批
* @param commitParam 审批参数
* @return 提交结果
*/
public String commitApproveApplyForOrder(WechatCommitParam commitParam, List<OrderProdVO> orderProdList, List<OrderAux> orderAuxList) {
// TODO 校验审批提交人与审批模板
//真正调用API的参数构造
Map<String, Object> reqParam = new HashMap<>();
// 发起人
reqParam.put("creator_userid", this.getUserId(commitParam.getPhone()));
// 审批模板
reqParam.put("template_id", commitParam.getTemplateId());
// 审批人模式:0-通过接口指定审批人、抄送人 1-使用模板设置
reqParam.put("use_template_approver", commitParam.getUseTemplateApprover());
// 抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
reqParam.put("notify_type", commitParam.getNotifyType());
// 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
reqParam.put("summary_list", this.setSummaryInfo(commitParam.getSummaryInfo()));
// 审批申请数据装配
JSONObject contents = this.getApplyDataForOrder(orderProdList, orderAuxList, commitParam.getApplyData());
reqParam.put("apply_data", contents);
// 发起审批
String url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN";
JSONObject commit = this.doPost(url, reqParam, JSONObject.class);
if (commit.getInteger("errcode").equals(IntegerPool.ZERO)) {
return commit.getString("sp_no");
}
throw new ServiceException("提交审批失败: " + commit.getString("errmsg"));
}
五、审批参数之参数构建
这部分就是对调用API的参数进行构建,特别需要核对控件ID与内容的一致性,避免使用SET,以免乱序导致的显示错行.
/**
* 根据模板填充审批数据 (订单) 正式环境
* @param applyData 审批数据内容
* @return JSONObject
*/
private JSONObject getApplyDataForProdOrder(List<OrderProdVO> orderProdList, List<OrderAux> orderAuxList, String... applyData){
JSONObject contents = new JSONObject();
// 订单基本信息
List<String> idList = new ArrayList<>();
idList.add("Text-11111111111111"); // 订单编号
idList.add("Text-22222222222222"); // 发货仓库
idList.add("Text-33333333333333"); // 客户名称
idList.add("Text-44444444444444"); // 联系人
idList.add("Text-55555555555555"); // 联系方式
idList.add("Text-66666666666666"); // 收货地址
idList.add("Text-77777777777777"); // 备注
JSONArray contentArray = new JSONArray();
for (int i = 0; i < idList.size() && i < applyData.length; i++){
JSONObject content = this.createContent(idList.get(i), applyData[i]);
contentArray.add(content);
}
// 订单明细
JSONObject prodTableValue = new JSONObject();
JSONArray prodChildren = new JSONArray();
if (Func.isNotEmpty(orderProdList)) {
for (OrderProdVO orderProd : orderProdList) {
JSONObject list = new JSONObject();
JSONArray row = new JSONArray();
// Text-1212121212 产品名称
JSONObject prodName = new JSONObject();
prodName.put("text", orderProd.getProdNameStr);
row.add(this.createControl("Text-1212121212", "Text", prodName));
//Money-23434343434 单价(元/袋) prodPrice
JSONObject price = new JSONObject();
price.put("new_money", orderProd.getProdPrice().stripTrailingZeros().toPlainString());
row.add(this.createControl("Money-23434343434", "Money", price));
// Number-454545454545数量(袋)
JSONObject num = new JSONObject();
num.put("new_number", orderProd.getProdNum().stripTrailingZeros().toPlainString());
row.add(this.createControl("Number-454545454545", "Number", num));
// Formula-222222222222222小计
row.add(this.createControl("Formula-222222222222222", "Formula", new JSONObject()));
list.put("list", row);
prodChildren.add(list);
}
prodTableValue.put("children", prodChildren);
JSONObject prodTable = new JSONObject();
prodTable.put("control", "Table");
prodTable.put("id", "Table-8888888888");
prodTable.put("value", prodTableValue);
contentArray.add(prodTable);
}
contents.put("contents", contentArray);
return contents;
}
六、控件构建
不同的控件用于显示不同数据,控件包含Text,Number,Money,Formula等,用于不同用途.具体用法参考官方文档,写的听详细了.
/**
* 创建Control
* @param id 控件ID
* @param control 控件类型
* @param value 内容
* @return content
*/
private JSONObject createControl(String id, String control, JSONObject value){
JSONObject content = new JSONObject();
content.put("control", control);
content.put("id", id);
content.put("value", value);
return content;
}
七、请求
将token换成真实token,去发起请求
/**
* post请求
* @param url 请求地址
* @param reqParam 请求参数
* @param tClass 请求返回值转换类型
*/
private <T> T doPost(String url, Map<String, Object> reqParam, Class<T> tClass){
String token = this.getToken();
String token_url = url.replace("ACCESS_TOKEN", token);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<T> responseEntity = restTemplate.postForEntity(token_url, reqParam, tClass);
//log.info("doPost请求地址 ==> {}", url);
log.info("doPost请求参数 ==> {}", reqParam);
//log.info("doPost获取结果 ==> {}", responseEntity);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return responseEntity.getBody();
}
throw new ServiceException(String.format("Post请求%s失败", url));
}
八、获取token
1.通过缓存获取token
/**
* 缓存Token
* @return Token
*/
public String getToken() {
// 先查询缓存 没有则请求接口
String redisTokenKey = "ACCESS_TOKEN_YL_SALE_TEST";
String tokenValue = redisCacheWechat.getCacheObject(redisTokenKey);
if (Func.isBlank(tokenValue)) {
Integer timeOut = 7200;
tokenValue = getAccessToken();
redisCacheWechat.setCacheObject(redisTokenKey, tokenValue, timeOut, TimeUnit.SECONDS);
log.info("接口请求Token");
}
return tokenValue;
}
2.通过接口获取token
/**
* 直接请求Token
* @return Token
*/
private String getAccessToken() {
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET"
.replace("ID", wechatProperties.getCorpId()).replace("SECRET", wechatProperties.getCorpSecret());
AccessTokenResult ac = this.doGet(url, AccessTokenResult.class);
log.info("获取企业微信Token结果 ==> {}", ac);
if (ac != null && ac.getErrcode() == 0) {
return ac.getAccess_token();
}
String error = ac == null ? "body is null" : ac.getErrmsg();
throw new ServiceException("获取微信token失败: " + error);
}
3.GET请求通用方法
/**
* get请求
* @param url 请求地址
* @param tClass 请求返回值转换类型
*/
private <T> T doGet(String url, Class<T> tClass){
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<T> responseEntity = restTemplate.getForEntity(url, tClass);
log.info("doGet请求地址 ==> {}", url);
log.info("doGet获取结果 ==> {}", responseEntity);
if (HttpStatus.OK == responseEntity.getStatusCode()) {
return responseEntity.getBody();
}
throw new ServiceException(String.format("Get请求%s失败", url));
}
4.缓存通用方法
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(prefix+":"+key, value, timeout, timeUnit);
}
九、回调,更新订单状态
我们是通过定时任务回调来获取审批结果的
/**
* 获取订单审批结果
* 状态 (0-新建; 1-已提交(企业微信待审核); 2-确认(企业微信审核同意); 3-部分发货; 4-发货完成; 5-订单完成; 6-审核驳回; 9-取消作废(已关闭))
* 审批通过创建销售出库单
*/
@Override
public void orderTask() {
List<Order> orderList = orderService.list(Wrappers.<Order>lambdaQuery()
.eq(Order::getOrderStatus, IntegerPool.ONE)
.eq(Order::getIsDeleted, IntegerPool.ZERO)
.isNotNull(Order::getWxSpNo));
if (orderList.isEmpty()){
return;
}
List<Order> updateList = new ArrayList<>();
for (Order order : orderList) {
Integer spStatus = weChatApproveUtil.getApprovalDetail(order.getWxSpNo());
// 审批通过
if (spStatus.equals(IntegerPool.TWO)){
// 更新订单状态
order.setOrderStatus(IntegerPool.TWO);
// TODO 创建销售出库单 已创建销售出库单的不继续执行
// 推送金蝶
if (kingDeeProperties.isEnable()) {
// TODO 推送金蝶逻辑
}
// 审核驳回
if (spStatus.equals(IntegerPool.THREE)){
order.setOrderStatus(IntegerPool.SIX);
// 更新客户余额 相当于退货了
// 恢复客户预扣款金额
// 整单解锁库存
}
}
orderService.updateBatchById(updateList);
}
回调函数,获取审批结果状态
/**
* 获取审批申请详情
* sp_status 申请单状态:1-审批中;2-已通过;3-已驳回;4-已撤销;6-通过后撤销;7-已删除;10-已支付
* @param spNo 审批单号
* @return sp_status
*/
public Integer getApprovalDetail(String spNo) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=ACCESS_TOKEN";
Map<String, Object> reqParam = new HashMap<>();
reqParam.put("sp_no", spNo);
JSONObject detail = this.doPost(url, reqParam, JSONObject.class);
System.out.println(detail);
if (detail.getInteger("errcode").equals(IntegerPool.ZERO)) {
JSONObject info = detail.getJSONObject("info");
return info.getInteger("sp_status");
}
throw new ServiceException("获取申请详情失败: " + detail.getString("errmsg"));
}