electripper/electripper-system/src/main/java/com/ruoyi/system/task/EtTask.java

528 lines
27 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.ruoyi.system.task;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.ServiceConstants;
import com.ruoyi.common.core.domain.entity.AsUser;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.map.GeoUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.service.*;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* 定时任务调度测试
*
* @author ruoyi
*/
@Slf4j
@Component("etTask")
public class EtTask {
@Resource
private EtOrderMapper etOrderMapper;
@Autowired
private IEtOperatingAreaService etOperatingAreaService;
@Resource
private SysUserMapper userMapper;
@Autowired
private IEtDividendDetailService dividendDetailService;
@Autowired
private ScheduledExecutorService scheduledExecutorService;
@Autowired
private IEtOrderService etOrderService;
@Resource
private AsDeviceMapper asDeviceMapper;
@Autowired
private IWxPayService wxPayService;
@Autowired
private IEtFeeRuleService etFeeRuleService;
@Autowired
private IEtRefundService etRefundService;
@Autowired
private RedisCache redisCache;
@Resource
private AsUserMapper asUserMapper;
@Autowired
private CallbackService callbackService;
@Resource
private EtLocationLogMapper etLocationLogMapper;
@Autowired
private IAsDeviceService deviceService;
/**
* 1.启动时判断是否有未取消预约的订单
* 2.判断已完成的订单未退还押金的
* 3.启动时判断是否分账
*/
@Transactional
@PostConstruct
public void init() {
log.info("=========================启动业务处理=========================");
log.info("=========================开始=========================");
/** 1.启动时判断是否有未取消预约的订单*/
// uncancelledAppointmentHandle();
/** 2.判断已完成的订单未退还押金的(根据et_refund表中的refund_result结果判断是否已经退款) */
/** ①找出所有已完成的订单 status=4 type = 1 r.refund_result IS NULL
* ②根据用户查询最后一次押金充值记录
*/
List<EtOrder> orders = etOrderMapper.selectUserListFinishOrder();
log.info("已完成的订单未退还押金的的订单 = " + JSON.toJSONString(orders));
for(EtOrder order:orders){
EtFeeRule rule = etFeeRuleService.selectEtFeeRuleByRuleIdIncludeDelete(order.getRuleId());
if(ObjectUtil.isNull(rule)){
throw new ServiceException("骑行订单:【"+order.getOrderNo()+"】未找到该套餐【"+order.getRuleId()+"");
}
EtOperatingArea area = etOperatingAreaService.selectEtOperatingAreaByAreaId(order.getAreaId());
AsUser asUser = asUserMapper.selectUserById(order.getUserId());
Integer autoRefundDeposit = rule.getAutoRefundDeposit();
// 根据用户查询最后一次押金充值订单
EtOrder etOrder = new EtOrder();
etOrder.setUserId(order.getUserId());
etOrder.setPaid(ServiceConstants.ORDER_PAY_STATUS_PAID);
etOrder.setType(ServiceConstants.ORDER_TYPE_DEPOSIT);
etOrder.setStatus(ServiceConstants.ORDER_STATUS_ORDER_END);
List<EtOrder> etOrders = etOrderMapper.selectEtOrderList(etOrder);
if (etOrders.size() > 0 || ObjectUtil.isNotNull(etOrders) ) {
Optional<EtOrder> latestOrderOptional = etOrders.stream()
.max(Comparator.comparing(EtOrder::getPayTime));
if (latestOrderOptional.isPresent()) {
EtOrder lastOrder = latestOrderOptional.get();
log.info("【系统启动】用户【{}】最后一次押金充值订单 : " + JSON.toJSONString(lastOrder),asUser.getUserId());
if(lastOrder.getTotalFee().compareTo(new BigDecimal(area.getDeposit()))!=0){
log.info("【系统启动】最后一次押金充值记录 金额与押金不一致,订单押金:【{}】,区域押金:【{}】",lastOrder.getTotalFee(),area.getDeposit());
}else{
// 根据最新的订单号,查询是否有退款记录
List<EtRefund> etRefunds = etRefundService.selectEtRefundByOrderNo(lastOrder.getOrderNo());
if(etRefunds.size() == 1){
EtRefund etRefund = etRefunds.get(0);
// 没有退款记录,发起退款
if(ObjectUtil.isNull(etRefund)){
// 根据订单支付时间 autoRefundDeposit个小时后退押金
String reason = autoRefundDeposit + "个小时后自动退押金";
Date payTime = order.getPayTime();
Date refundDepositTime = DateUtils.getTimeAfterXHours(payTime, autoRefundDeposit);
Date nowDate = DateUtils.getNowDate();
if (nowDate.after(refundDepositTime)) {
log.info("【系统启动】用户【{}】押金充值订单【{}】已过期,开始自动退押金",asUser.getUserId(),lastOrder.getOrderNo());
refundDeposit(asUser, lastOrder, reason);
}else{
int timeDifferenceInMinutes = DateUtils.timeDifferenceInMinutes(payTime, nowDate);
int i = autoRefundDeposit * 60;
int delay = i - timeDifferenceInMinutes;
log.info("【系统启动】用户【{}】押金充值订单【{}】未过期,【{}】分钟后退押金",asUser.getUserId(),lastOrder.getOrderNo(),delay);
scheduledExecutorService.schedule(() -> {
refundDeposit(asUser, lastOrder, reason);
}, delay, TimeUnit.MINUTES);
}
}else{
// 有退款记录,判断是否成功
if(!Constants.SUCCESS2.equals(etRefund.getRefundResult())){
log.info("【系统启动】押金退款未成功回调,退款单号:【{}】",etRefund.getRefundNo());
// 根据退款单号查询退款信息
Refund refund = wxPayService.queryByOutRefundNo(area.getAreaId(),etRefund.getRefundNo());
if(ObjectUtil.isNotNull(refund) && Constants.SUCCESS2.equals(refund.getStatus().name())){
// 更新退款记录
etRefund.setRefundResult(Constants.SUCCESS2);
etRefund.setUpdateTime(new Date());
etRefundService.updateEtRefund(etRefund);
log.info("【系统启动】更新押金退款回调成功,退款单号:【{}】",refund.getOutRefundNo());
}
}
}
}
}
}
}
}
/** 3.启动时判断是否分账(根据订单号查询分账明细表是否有记录来判断是否分账) */
/** ①找出所有已完成的骑行订单 status=4 type = 1 r.refund_result IS NULL
* ②根据订单号查询分账明细表是否有记录
* 有记录则已经分账过
* 没值代表还未分账
* 判断是否已过分账时间
* 未过,计算出多少小时后分账
* 已过,直接分账(记录分账明细表)
*/
// 查询所有待分账的订单
// List<EtOrder> needDividendOrders = etOrderMapper.selectNeedDividendOrder();
// for(EtOrder order: needDividendOrders){
// log.info("【系统启动】待分账订单:【{}】",order.getOrderNo());
// EtOperatingArea area = etOperatingAreaService.selectEtOperatingAreaByAreaId(order.getAreaId());
// if(dividendDetailService.isDividendComputedByOrderNo(order.getOrderNo())){
// log.info("订单【{}】已经分账",order.getOrderNo());
// break;
// }
// log.info("【系统启动】骑行订单【{}】未分账,开始分账",order.getOrderNo());
// Date payTime = order.getPayTime();
// Date dividendTime = DateUtils.getTimeAfterXHours(payTime, 24);//分账时间
// Date nowDate = DateUtils.getNowDate();
// if (nowDate.after(dividendTime)) {
// log.info("【系统启动】骑行订单【{}】已过分账时间,开始分账",order.getOrderNo());
// // 请求分账处理
// Transaction transaction = wxPayService.queryOrderByOutTradeNo(order.getOrderNo());
//// if (callbackService.dividendHandle(transaction.getTransactionId(), order, area)) break;
// }else{
// int timeDifferenceInHours = DateUtils.timeDifferenceInHours(payTime, nowDate);
// int delay = 24 - timeDifferenceInHours;
// log.info("【系统启动】骑行订单【{}】未过分账时间,【{}】小时后开始分账",order.getOrderNo(),delay);
// // 24小时后发起分账
//// scheduledExecutorService.schedule(() -> {
//// // 请求分账处理
//// Transaction transaction = wxPayService.queryOrderByOutTradeNo(order.getOrderNo());
//// if (callbackService.dividendHandle(transaction.getTransactionId(), order, area)) return;
//// }, delay , TimeUnit.HOURS);
// }
// }
// log.info("=========================结束=========================");
}
private void refundDeposit(AsUser asUser, EtOrder lastOrder, String reason) {
String outRefundNo = IdUtils.getOrderNo("ref");
lastOrder.setReason(reason);
EtRefund refund1= etOrderService.createRefund(lastOrder, lastOrder.getTotalFee(), null, null, null, null, outRefundNo,ServiceConstants.REFUND_TYPE_DEPOSIT);
if(etRefundService.insertEtRefund(refund1)>0){
log.info("【自动退款】保存退款对象成功");
// 新增资金流水记录
// callbackService.capitalFlowRecords(lastOrder,ServiceConstants.FLOW_TYPE_DISBURSE,ServiceConstants.ORDER_TYPE_DEPOSIT_REFUND);
// 更新用户信息,清除缓存
asUser.setBalance(BigDecimal.ZERO);
int updateUser = asUserMapper.updateUser(asUser);
if(updateUser>0){
// Collection<String> keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.APP_LOGIN_TOKEN_KEY + "*");
// redisCache.deleteObject(keys);
log.info("【系统启动】退还押金,更新用户余额成功!");
}
Refund refund = wxPayService.refund(lastOrder, reason, lastOrder.getTotalFee(),outRefundNo);
log.info("=================【系统启动】退还押金定时任务结束!!!==================");
}else{
throw new ServiceException("【系统启动】保存退款对象失败");
}
}
private void uncancelledAppointmentHandle() {
List<EtOrder> orders= etOrderMapper.selectAppointmentUnfinished();
log.info("预约未完成的订单 = " + JSON.toJSONString(orders));
for (EtOrder order:orders) {
EtOperatingArea area = etOperatingAreaService.selectEtOperatingAreaByAreaId(order.getAreaId());
AsDevice asDevice = asDeviceMapper.selectAsDeviceBySn(order.getSn());
Date appointmentEndTime = DateUtils.getTimeAfterXMinutes(order.getAppointmentStartTime(), area.getTimeoutMinutes());//预约结束时间
int timeDifferenceInSeconds = DateUtils.timeDifferenceInSeconds(appointmentEndTime, order.getAppointmentStartTime());//(超时时间-开始时间)的秒数
int differenceInSeconds = DateUtils.timeDifferenceInSeconds(new Date(), order.getAppointmentStartTime());//(当前时间-开始时间)的秒数
int delay = timeDifferenceInSeconds - differenceInSeconds;
log.info("【定时取消预约】延迟:【{}】秒", delay);
//定时取消预约
scheduledExecutorService.schedule(() -> {
log.error("【车辆超时预约】系统自动取消");
EtOrder order1 = etOrderService.selectEtOrderByOrderNo(order.getOrderNo());
log.info("【定时取消预约】重新获取订单信息:{}",JSON.toJSON(order1));
if(order1.getPaid().equals(ServiceConstants.ORDER_PAY_STATUS_PAID)){//已支付订单,跳过
log.error("【车辆超时预约】订单已支付,跳过");
return;
}
log.error("【车辆超时预约】订单未支付,系统自动处理");
//未支付 订单更新最后预约时间,并结束订单,做超出预约时间标记
order.setStatus(ServiceConstants.ORDER_STATUS_CANCEL_APPOINTMENT);
order.setAppointmentEndTime(new Date());
order.setAppointmentTimeout("1");
//计算预约费
BigDecimal appointmentServiceFee = area.getAppointmentServiceFee();
BigDecimal fee = appointmentServiceFee.multiply(new BigDecimal(area.getTimeoutMinutes()).divide(new BigDecimal(10)));
order.setAppointmentFee(fee);
order.setTotalFee(fee);
order.setPayFee(fee);
int update = etOrderService.updateEtOrder(order);
if(update==0){
throw new ServiceException("【车辆超时预约】:更新订单状态失败");
}
// 改变车辆状态
asDevice.setStatus(ServiceConstants.VEHICLE_STATUS_NORMAL);
asDevice.setLockStatus(ServiceConstants.LOCK_STATUS_OPEN);
int device = asDeviceMapper.updateAsDevice(asDevice);
if(device==0){
log.error("【车辆超时预约】更新车辆状态失败");
throw new ServiceException("【车辆超时预约】更新车辆状态失败");
}
}, delay, TimeUnit.SECONDS);
}
}
/**
* 每天凌晨0点5分执行计算分账结果
* cron: 0 5 0 * * ?
*/
public void computeDividend()
{
log.info("每天凌晨0点5分执行计算分账结果");
// 获取昨天的订单,2024-05-26 00:00:00 -- 2024-05-26 23:59:59
// 获取昨天日期格式: yyyy-MM-dd
LocalDate yesterday = LocalDate.now().minusDays(1);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedYesterday = yesterday.format(formatter);
log.info("获取昨天日期 = " + formattedYesterday);
//判断该日期是否已经计算过分账结果
if(dividendDetailService.isDividendComputed(LocalDate.now().format(formatter))){
log.info("该日期已经计算过分账结果");
return;
}
String startDateStr = formattedYesterday + " "+ Constants.DATE_FORMAT_START_PEREND;
String endDateStr = formattedYesterday + " " +Constants.DATE_FORMAT_END_PEREND;
EtOrder order = new EtOrder();
order.setStartTime(startDateStr);
order.setEndTime(endDateStr);
order.setPaid(ServiceConstants.ORDER_PAY_STATUS_PAID);
order.setStatus(ServiceConstants.ORDER_STATUS_ORDER_END);
order.setType(ServiceConstants.ORDER_TYPE_RIDING);
List<EtOrder> orderListByDate = etOrderMapper.selectEtOrderList(order);
for(EtOrder order1:orderListByDate){
EtDividendDetail etDividendDetail = new EtDividendDetail();
EtOperatingArea area = etOperatingAreaService.selectEtOperatingAreaByAreaId(order1.getAreaId());
SysUser sysUser = new SysUser();
sysUser.setUserType("03");
sysUser.setAreaId(area.getAreaId());
List<SysUser> sysUsers = userMapper.selectUserList(sysUser);
for(SysUser user : sysUsers){
etDividendDetail.setAreaId(area.getAreaId());
etDividendDetail.setPartnerId(user.getUserId());
etDividendDetail.setOrderNo(order1.getOrderNo());
etDividendDetail.setTotalAmount(order1.getTotalFee());
etDividendDetail.setCreateTime(DateUtils.getNowDate());
etDividendDetail.setDividendProportion(user.getDividendProportion());
String dividendItem = user.getDividendItem();
// todo 分账金额是骑行费,还是调度费,看分账项目 分账项目1-骑行费(骑行费+预约费2-调度费(调度费+管理费)
BigDecimal dividendAmount = BigDecimal.ZERO;
if(dividendItem.contains("1")){
dividendAmount.add(order1.getRidingFee().add(order1.getAppointmentFee()));//1-骑行费(骑行费+预约费)
}else if(dividendItem.contains("2")){
dividendAmount.add(order1.getManageFee().add(order1.getManageFee()));//2-调度费(调度费+停车点外调度费)
}
BigDecimal divide = new BigDecimal(user.getDividendProportion()).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
etDividendDetail.setDividendAmount(dividendAmount.multiply(divide));
etDividendDetail.setDividendItem(dividendItem);
log.info("保存分账明细 === " + JSON.toJSONString(etDividendDetail));
int i = dividendDetailService.insertEtDividendDetail(etDividendDetail);
if(i==0){
throw new ServiceException("保存分账明细失败");
}
}
int totalDividendProportion = IntStream.of(sysUsers.stream()
.mapToInt(SysUser::getDividendProportion)
.toArray())
.sum();
//算运营商自己的分账
etDividendDetail.setAreaId(area.getAreaId());
etDividendDetail.setPartnerId(0L);
etDividendDetail.setOrderNo(order1.getOrderNo());
etDividendDetail.setTotalAmount(order1.getTotalFee());
etDividendDetail.setCreateTime(DateUtils.getNowDate());
etDividendDetail.setDividendAmount(order1.getTotalFee().multiply(new BigDecimal(100-totalDividendProportion).divide(new BigDecimal(100),2, BigDecimal.ROUND_HALF_UP)));
etDividendDetail.setDividendProportion(100-totalDividendProportion);
etDividendDetail.setDividendItem("运营商");
int i = dividendDetailService.insertEtDividendDetail(etDividendDetail);
if(i==0){
throw new ServiceException("保存分账明细失败");
}
}
}
/**
* 开始骑行未结束的订单1分钟算一次距离
* cron: 0 5 0 * * ?
*/
public void computeDistance(){
log.info("-------------------【定时任务】计算订单距离开始-------------------");
EtOrder order = new EtOrder();
order.setType("1");
order.setStatus(ServiceConstants.ORDER_STATUS_RIDING);
List<EtOrder> orders = etOrderService.selectEtOrderList(order);
for(EtOrder etOrder:orders){
String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getNowDate());
String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, etOrder.getUnlockTime());
String tripRouteStr = deviceService.trajectory(etOrder.getSn(), startTime, endTime);
if(StrUtil.isNotBlank(tripRouteStr)){
double[][] doubles = GeoUtils.parseJsonTrack(tripRouteStr);
double v = GeoUtils.calculateTotalDistance(doubles);
etOrder.setDistance((int)Math.round(v));
int updateEtOrder = etOrderService.updateEtOrder(etOrder);
if(updateEtOrder>0){
log.info("【定时任务】计算订单距离成功【orderNo="+etOrder.getOrderNo()+"");
}
}
}
}
/**
* 一个星期删除一次onenet心跳日志
* cron: 0 5 0 * * ?
*
* DELETE FROM et_location_log
* WHERE create_time < NOW() - INTERVAL 7 DAY;
*/
public void deleteLocationLog(){
log.info("-------------------【定时任务】删除onenet心跳日志-------------------");
etLocationLogMapper.deleteLocationLogByCreateTime();
}
/**
* 车辆与订单状态同步
* 1. 如果有正在骑行中的订单,车辆的状态是待骑行的,改成临时锁车,不发命令
* 2. 如果车辆状态是骑行中或临时锁车,查询订单没有订单,则修改车辆状态为待骑行
*
*/
public void stausSynchronization(){
log.info("-------------------【定时任务】车辆与订单状态同步-------------------");
// 当前有骑行中的订单
List<EtOrder> orders = etOrderService.getCurrentOrderList();
for (EtOrder order:orders) {
AsDevice device = asDeviceMapper.selectAsDeviceBySn(order.getSn());
if(ObjectUtil.isNotNull(device) && device.getStatus().equals(ServiceConstants.VEHICLE_STATUS_NORMAL)){
AsDevice device1 = new AsDevice();
device1.setSn(device.getSn());
device1.setStatus(ServiceConstants.VEHICLE_STATUS_TEMPORARILY_LOCK);
device1.setLockStatus(ServiceConstants.LOCK_STATUS_CLOSE);
int i = asDeviceMapper.updateAsDeviceBySn(device1);
if(i>0){
log.info("【定时任务】车辆状态修改为临时锁车【sn="+device.getSn()+"");
}
}
}
// 2. 如果车辆状态是骑行中或临时锁车,查询当前没有订单,则修改车辆状态为待骑行
QueryWrapper<AsDevice> wrapper = new QueryWrapper<>();
wrapper.in("status", "3","4"); // 设备状态正常
// 查询所有设备
List<AsDevice> allDevices = asDeviceMapper.selectList(wrapper);
for(AsDevice device:allDevices){
if(ObjectUtil.isNotNull(etOrderService.getCurrentOrder2(device.getSn()))){
continue;
}else{
AsDevice device1 = new AsDevice();
device1.setSn(device.getSn());
device1.setStatus(ServiceConstants.VEHICLE_STATUS_NORMAL);
device1.setLockStatus(ServiceConstants.LOCK_STATUS_CLOSE);
int i = asDeviceMapper.updateAsDeviceBySn(device1);
if(i>0){
log.info("【定时任务】车辆状态修改为待骑行【sn="+device.getSn()+"");
}
}
}
}
// 写一个定时,如果车辆是骑行中,没有现在骑行中的订单,则关闭车辆
/**
* 自动押金抵扣
* 写一个定时任务处理所有的 7天前待支付的订单用押金抵扣如果已经退押金的直接改成结束订单
* 1. 查询所有待支付的订单根据还车时间7天前的订单
* 2. 如果订单金额是0直接结束订单修改订单状态为已支付
* 3. 查询用户是否还有未退款的押金,如果有,则进行押金抵扣,如果没有,则结束订单
*/
public void autoDeduction(){
log.info("-------------------【定时任务】自动押金抵扣-------------------");
/** 1. 查询所有待支付的订单根据还车时间7天前的订单 */
List<EtOrder> orders = etOrderMapper.selectToBePaidEtOrderList();
if(ObjectUtil.isNotNull(orders) && orders.size()>0){
for(EtOrder order:orders){
if(order.getTotalFee().compareTo(BigDecimal.ZERO) == 0){
// 结束订单,修改订单状态为已支付
updateOrderPaid(order);
}else{
etOrderService.deduction(order);
}
}
}
}
/**
* 押金抵扣不成功的修复
* 1. 找出所有押金抵扣不成功的订单
* 2. 将状态改成已结束
*
*/
public void deductionErrorOrder(){
log.info("-------------------【定时任务】押金抵扣不成功的修复---开始----------------");
List<EtOrder> orders = etOrderMapper.deductionErrorOrderList();
for (EtOrder order:orders) {
EtOrder order1 = new EtOrder();
order1.setOrderId(order.getOrderId());
order1.setStatus(ServiceConstants.ORDER_STATUS_ORDER_END);
order1.setMark("押金抵扣修复,订单已结束");
etOrderMapper.updateEtOrder(order1);
}
log.info("-------------------【定时任务】押金抵扣不成功的修复---结束----------------");
}
/** 更新订单为已支付*/
private void updateOrderPaid(EtOrder order) {
order.setPaid(ServiceConstants.ORDER_PAY_STATUS_PAID);
order.setPayTime(DateUtils.getNowDate());
order.setStatus(ServiceConstants.ORDER_STATUS_ORDER_END);
order.setPayType(ServiceConstants.PAY_TYPE_YJ);
order.setMark("超过7天系统自动押金抵扣");
order.setDepositDeduction(ServiceConstants.IS_DEPOSIT_DEDUCTION);
int updateEtOrder = etOrderMapper.updateEtOrder(order);
if(updateEtOrder == 0){
throw new ServiceException("押金抵扣失败,更新骑行订单失败");
}
}
/** 如果还有未退款的押金,如果有,则进行押金抵扣 */
private void autoDeductionHandle(EtOrder order){
// select * from et_order o
// where o.status ='4' and o.paid = '1' and o.type = 1 and o.is_test = '0'
// GROUP BY o.user_id
}
}