修改同步计费
This commit is contained in:
+166
-1
@@ -1,24 +1,44 @@
|
||||
package com.bms.web.controller.monitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.HttpStatus;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.PageDomain;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.PrcBillingTask;
|
||||
import com.bms.price.domain.PrcBillingTaskRetryTask;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
import com.bms.price.oracle.TabOrderMonitorService;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
import com.bms.price.task.TabOrderSyncAsyncRunner;
|
||||
|
||||
/**
|
||||
* 订单数据源:查询上游 Oracle TAB_ORDER。
|
||||
* 订单数据源:查询上游 Oracle TAB_ORDER,并分批同步计费。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/orderSource")
|
||||
@@ -27,6 +47,15 @@ public class TabOrderMonitorController extends BaseController
|
||||
@Autowired
|
||||
private TabOrderMonitorService tabOrderMonitorService;
|
||||
|
||||
@Autowired
|
||||
private IPrcBillingTaskService billingTaskService;
|
||||
|
||||
@Autowired
|
||||
private TabOrderSyncAsyncRunner tabOrderSyncAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(TabOrderMonitorQuery query)
|
||||
@@ -52,6 +81,142 @@ public class TabOrderMonitorController extends BaseController
|
||||
return rsp;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:sync')")
|
||||
@Log(title = "order source sync filter", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/syncByFilter")
|
||||
public AjaxResult syncByFilter(@RequestBody TabOrderMonitorQuery query)
|
||||
{
|
||||
if (!hasRequiredFilters(query))
|
||||
{
|
||||
return error("customerCodes and pickup time required");
|
||||
}
|
||||
tabOrderMonitorService.count(query);
|
||||
String asyncTaskId = UUID.randomUUID().toString().replace("-", "");
|
||||
tabOrderSyncAsyncRunner.put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_PENDING, 0, "queued",
|
||||
0, 0, 0, 0, null);
|
||||
tabOrderSyncAsyncRunner.run(asyncTaskId, query, TabOrderSyncAsyncRunner.clampBatch(query.getBatchSize()));
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("asyncTaskId", asyncTaskId);
|
||||
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "FILTER", query, data, "SUCCESS", null);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:sync') or @ss.hasPermi('monitor:orderSource:list')")
|
||||
@GetMapping("/syncTask/{asyncTaskId}")
|
||||
public AjaxResult syncTaskProgress(@PathVariable("asyncTaskId") String asyncTaskId)
|
||||
{
|
||||
PrcBillingTaskRetryTask task = tabOrderSyncAsyncRunner.get(asyncTaskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error("sync task not found or expired");
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:sync')")
|
||||
@Log(title = "order source sync selected", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/sync")
|
||||
public AjaxResult syncSelected(@RequestBody List<TabOrderMonitorVo> rows)
|
||||
{
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
return error("rows required");
|
||||
}
|
||||
if (rows.size() > 200)
|
||||
{
|
||||
return error("selected rows exceed 200");
|
||||
}
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
List<Map<String, Object>> details = new ArrayList<Map<String, Object>>();
|
||||
for (TabOrderMonitorVo row : rows)
|
||||
{
|
||||
Map<String, Object> item = new HashMap<String, Object>();
|
||||
item.put("orderId", row == null ? null : row.getOrderId());
|
||||
item.put("transferOrderId", row == null ? null : row.getTransferOrderId());
|
||||
try
|
||||
{
|
||||
PrcBillingTask task = billingTaskService.submitAndProcess(TabOrderSyncAsyncRunner.toSubmit(row));
|
||||
item.put("successStatus", task == null ? null : task.getSuccessStatus());
|
||||
item.put("errorMessage", task == null ? null : task.getErrorMessage());
|
||||
if (task != null && BillTaskSuccessStatus.SUCCESS.getCode().equals(task.getSuccessStatus()))
|
||||
{
|
||||
ok++;
|
||||
}
|
||||
else
|
||||
{
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
fail++;
|
||||
item.put("successStatus", "ERROR");
|
||||
item.put("errorMessage", e.getMessage());
|
||||
}
|
||||
details.add(item);
|
||||
}
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("ok", Integer.valueOf(ok));
|
||||
data.put("fail", Integer.valueOf(fail));
|
||||
data.put("details", details);
|
||||
String action = rows.size() == 1 ? "ONE" : "SELECTED";
|
||||
String bizId = firstOrderBizId(rows, action);
|
||||
String status = fail > 0 && ok == 0 ? "FAIL" : "SUCCESS";
|
||||
String err = fail > 0 ? ("fail=" + fail) : null;
|
||||
TabOrderSyncEsLog.save(billSearchStore, bizId, action, rows, data, status, err);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@GetMapping("/syncLog/list")
|
||||
public TableDataInfo syncLogList(
|
||||
@RequestParam(value = "bizId", required = false) String bizId,
|
||||
@RequestParam(value = "status", required = false) String status)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.ORDER_SOURCE_SYNC, bizId, status, pageNum, pageSize);
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("\u67e5\u8be2\u6210\u529f");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:list')")
|
||||
@GetMapping("/syncLog/{docId}")
|
||||
public AjaxResult syncLogInfo(@PathVariable("docId") String docId)
|
||||
{
|
||||
return success(billSearchStore.getByDocId(docId));
|
||||
}
|
||||
|
||||
private static String firstOrderBizId(List<TabOrderMonitorVo> rows, String action)
|
||||
{
|
||||
String prefix = action == null ? "SYNC" : action;
|
||||
if (rows != null)
|
||||
{
|
||||
for (TabOrderMonitorVo row : rows)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getOrderId()))
|
||||
{
|
||||
return prefix + "_" + row.getOrderId();
|
||||
}
|
||||
if (StringUtils.isNotEmpty(row.getTransferOrderId()))
|
||||
{
|
||||
return prefix + "_" + row.getTransferOrderId();
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefix + "_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private static boolean hasRequiredFilters(TabOrderMonitorQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
|
||||
@@ -25,4 +25,6 @@ public class TabOrderMonitorQuery implements Serializable
|
||||
private String orgOrderId;
|
||||
/** 转单号 TRANSFER_ORDER_ID */
|
||||
private String transferOrderId;
|
||||
/** 分批同步计费每批条数 */
|
||||
private Integer batchSize;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public final class EsLogType
|
||||
public static final String CREDIT = "CREDIT";
|
||||
/** Clear order credit arrears after balance in */
|
||||
public static final String ARREARS = "ARREARS";
|
||||
/** Monitor: sync billing from TAB_ORDER */
|
||||
public static final String ORDER_SOURCE_SYNC = "ORDER_SOURCE_SYNC";
|
||||
|
||||
private EsLogType()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.bms.price.search;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
|
||||
/** Write order-source sync actions to ES. */
|
||||
public final class TabOrderSyncEsLog
|
||||
{
|
||||
private TabOrderSyncEsLog()
|
||||
{
|
||||
}
|
||||
|
||||
public static String operator()
|
||||
{
|
||||
try
|
||||
{
|
||||
String name = SecurityUtils.getUsername();
|
||||
return StringUtils.isEmpty(name) ? "unknown" : name;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(IBillSearchStore store, String bizId, String action, Object request, Object response,
|
||||
String status, String errorMessage)
|
||||
{
|
||||
if (store == null || StringUtils.isEmpty(bizId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Map<String, Object> req = new LinkedHashMap<String, Object>();
|
||||
req.put("action", action);
|
||||
req.put("operator", operator());
|
||||
req.put("payload", request);
|
||||
String reqJson = JSON.toJSONString(req);
|
||||
String respJson = response == null ? null : JSON.toJSONString(response);
|
||||
String st = StringUtils.isEmpty(status) ? EsBizLogSupport.STATUS_SUCCESS : status;
|
||||
store.save(EsLogType.ORDER_SOURCE_SYNC, bizId, reqJson, respJson, st, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ public interface IPrcBillingTaskService
|
||||
/** Warehouse inbound: create task + publish MQ. */
|
||||
PrcBillingTask submitInbound(BillingTaskSubmitRequest request);
|
||||
|
||||
/** Monitor sync: create task then process immediately (no extra MQ). */
|
||||
PrcBillingTask submitAndProcess(BillingTaskSubmitRequest request);
|
||||
|
||||
/** MQ consumer entry: Oracle query -> bill -> writeback success status. */
|
||||
void processTask(Long taskId);
|
||||
|
||||
|
||||
+46
@@ -106,6 +106,52 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrcBillingTask submitAndProcess(BillingTaskSubmitRequest request)
|
||||
{
|
||||
PrcBillingTask task = createWaitingTask(request, "monitor");
|
||||
processTask(task.getTaskId());
|
||||
return billingTaskMapper.selectPrcBillingTaskById(task.getTaskId());
|
||||
}
|
||||
|
||||
private PrcBillingTask createWaitingTask(BillingTaskSubmitRequest request, String createBy)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ServiceException("billing task request required");
|
||||
}
|
||||
String transferNo = trim(request.getTransferNo());
|
||||
String trackingNo = trim(request.getTrackingNo());
|
||||
String orderNo = trim(request.getOrderNo());
|
||||
Date billTime = request.getBillTime();
|
||||
if (StringUtils.isEmpty(transferNo) && StringUtils.isEmpty(trackingNo) && StringUtils.isEmpty(orderNo))
|
||||
{
|
||||
throw new ServiceException("transferNo or trackingNo or orderNo required");
|
||||
}
|
||||
if (billTime == null)
|
||||
{
|
||||
throw new ServiceException("billTime required (billing time from mobile)");
|
||||
}
|
||||
BillTaskType type = BillTaskType.fromCode(request.getBillType());
|
||||
if (type == null)
|
||||
{
|
||||
type = resolveAutoType(transferNo, trackingNo, orderNo);
|
||||
}
|
||||
PrcBillingTask task = new PrcBillingTask();
|
||||
task.setTransferNo(transferNo);
|
||||
task.setTrackingNo(trackingNo);
|
||||
task.setOrderNo(orderNo);
|
||||
task.setCustomerCode(trim(request.getCustomerCode()));
|
||||
task.setBillTime(billTime);
|
||||
task.setBillType(type.getCode());
|
||||
task.setSuccessStatus(BillTaskSuccessStatus.WAITING.getCode());
|
||||
task.setCreateBy(StringUtils.isEmpty(createBy) ? "tms" : createBy);
|
||||
billingTaskMapper.insertPrcBillingTask(task);
|
||||
log.info("billing task created taskId={} type={} billTime={} customerCode={} transferNo={} trackingNo={} orderNo={}",
|
||||
task.getTaskId(), task.getBillType(), billTime, task.getCustomerCode(), transferNo, trackingNo, orderNo);
|
||||
return task;
|
||||
}
|
||||
|
||||
private static final int BILL_DB_RETRY_MAX = 3;
|
||||
private static final long BILL_DB_RETRY_BASE_MS = 80L;
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.bms.price.task;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.common.core.redis.RedisCache;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.PrcBillingTask;
|
||||
import com.bms.price.domain.PrcBillingTaskRetryTask;
|
||||
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
import com.bms.price.oracle.TabOrderMonitorService;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
|
||||
/**
|
||||
* Page Oracle TAB_ORDER then submitAndProcess each row.
|
||||
*/
|
||||
@Component
|
||||
public class TabOrderSyncAsyncRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(TabOrderSyncAsyncRunner.class);
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "billing:orderSource:sync:";
|
||||
public static final int TTL_MINUTES = 120;
|
||||
public static final int DEFAULT_BATCH = 50;
|
||||
public static final int MIN_BATCH = 20;
|
||||
public static final int MAX_BATCH = 200;
|
||||
|
||||
@Autowired
|
||||
private TabOrderMonitorService tabOrderMonitorService;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IPrcBillingTaskService billingTaskService;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String asyncTaskId, TabOrderMonitorQuery query, int batchSize)
|
||||
{
|
||||
int batch = clampBatch(batchSize);
|
||||
try
|
||||
{
|
||||
long totalLong = tabOrderMonitorService.count(query);
|
||||
int total = totalLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) totalLong;
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_RUNNING, 0, "starting", total, 0, 0, 0, null);
|
||||
if (total <= 0)
|
||||
{
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_SUCCESS, 100, "no tasks", 0, 0, 0, 0, null);
|
||||
return;
|
||||
}
|
||||
int processed = 0;
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
int pageNum = 1;
|
||||
while (true)
|
||||
{
|
||||
List<TabOrderMonitorVo> rows = tabOrderMonitorService.selectPage(query, pageNum, batch);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_RUNNING,
|
||||
Math.min(99, (int) ((processed * 100L) / Math.max(1, total))),
|
||||
"processing " + processed + "/" + total, total, processed, ok, fail, null);
|
||||
for (TabOrderMonitorVo row : rows)
|
||||
{
|
||||
try
|
||||
{
|
||||
PrcBillingTask task = billingTaskService.submitAndProcess(toSubmit(row));
|
||||
if (task != null && BillTaskSuccessStatus.SUCCESS.getCode().equals(task.getSuccessStatus()))
|
||||
{
|
||||
ok++;
|
||||
}
|
||||
else
|
||||
{
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
fail++;
|
||||
log.warn("orderSource sync failed orderId={} transfer={} msg={}",
|
||||
row == null ? null : row.getOrderId(),
|
||||
row == null ? null : row.getTransferOrderId(),
|
||||
e.getMessage());
|
||||
}
|
||||
processed++;
|
||||
int percent = Math.min(99, (int) ((processed * 100L) / Math.max(1, total)));
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_RUNNING, percent,
|
||||
"processing " + processed + "/" + total, total, processed, ok, fail, null);
|
||||
}
|
||||
if (rows.size() < batch)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_SUCCESS, 100,
|
||||
"done ok=" + ok + " fail=" + fail, total, processed, ok, fail, null);
|
||||
java.util.Map<String, Object> resp = new java.util.LinkedHashMap<String, Object>();
|
||||
resp.put("asyncTaskId", asyncTaskId);
|
||||
resp.put("total", Integer.valueOf(total));
|
||||
resp.put("processed", Integer.valueOf(processed));
|
||||
resp.put("ok", Integer.valueOf(ok));
|
||||
resp.put("fail", Integer.valueOf(fail));
|
||||
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "FILTER_DONE", query, resp, "SUCCESS", null);
|
||||
log.info("orderSource sync done asyncTaskId={} total={} ok={} fail={}", asyncTaskId, total, ok, fail);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("orderSource sync failed asyncTaskId={}", asyncTaskId, e);
|
||||
String msg = e.getMessage();
|
||||
if (StringUtils.isEmpty(msg))
|
||||
{
|
||||
msg = e.getClass().getSimpleName();
|
||||
}
|
||||
put(asyncTaskId, PrcBillingTaskRetryTask.STATUS_FAIL, 100, "failed", 0, 0, 0, 0, msg);
|
||||
TabOrderSyncEsLog.save(billSearchStore, asyncTaskId, "FILTER_DONE", query, null, "FAIL", msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String taskId, String status, int percent, String message,
|
||||
int total, int processed, int ok, int fail, String error)
|
||||
{
|
||||
PrcBillingTaskRetryTask task = new PrcBillingTaskRetryTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setStatus(status);
|
||||
task.setPercent(percent);
|
||||
task.setMessage(message);
|
||||
task.setTotal(total);
|
||||
task.setProcessed(processed);
|
||||
task.setOk(ok);
|
||||
task.setFail(fail);
|
||||
task.setError(error);
|
||||
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
public PrcBillingTaskRetryTask get(String taskId)
|
||||
{
|
||||
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
||||
}
|
||||
|
||||
public static int clampBatch(Integer batchSize)
|
||||
{
|
||||
int n = batchSize == null ? DEFAULT_BATCH : batchSize.intValue();
|
||||
if (n < MIN_BATCH)
|
||||
{
|
||||
return MIN_BATCH;
|
||||
}
|
||||
if (n > MAX_BATCH)
|
||||
{
|
||||
return MAX_BATCH;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
public static BillingTaskSubmitRequest toSubmit(TabOrderMonitorVo row)
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
throw new com.bms.common.exception.ServiceException("order row required");
|
||||
}
|
||||
BillingTaskSubmitRequest req = new BillingTaskSubmitRequest();
|
||||
req.setTransferNo(row.getTransferOrderId());
|
||||
req.setTrackingNo(row.getOrgOrderId());
|
||||
req.setOrderNo(row.getOrderId());
|
||||
req.setCustomerCode(row.getCustomerCode());
|
||||
req.setBillTime(row.getPickupDate());
|
||||
return req;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
|
||||
SELECT 2326, N'查询', 2325, 1, '', NULL, 1, 0, 'F', '0', '0', 'monitor:orderSource:list', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2326);
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2327, N'同步计费', 2325, 2, '', NULL, 1, 0, 'F', '0', '0', 'monitor:orderSource:sync', '#', 'admin', sysdate(), N'按筛选分页同步上游订单计费'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2327 OR perms = 'monitor:orderSource:sync');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id FROM (SELECT 2325 AS menu_id UNION SELECT 2326) m
|
||||
SELECT 1, m.menu_id FROM (SELECT 2325 AS menu_id UNION SELECT 2326 UNION SELECT 2327) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 订单数据源 同步计费按钮
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2327, N'同步计费', 2325, 2, '', NULL, 1, 0, 'F', '0', '0', 'monitor:orderSource:sync', '#', 'admin', sysdate(), N'按筛选分页同步上游订单计费'
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2327 OR perms = 'monitor:orderSource:sync');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, 2327 FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = 2327);
|
||||
@@ -8,3 +8,43 @@ export function listOrderSource(query) {
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
export function syncOrderSourceByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncByFilter',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderSourceSyncProgress(asyncTaskId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncTask/' + asyncTaskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function syncOrderSourceSelected(rows) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/sync',
|
||||
method: 'post',
|
||||
data: rows,
|
||||
timeout: 180000
|
||||
})
|
||||
}
|
||||
|
||||
export function listOrderSourceSyncLog(query) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncLog/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderSourceSyncLog(docId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncLog/' + encodeURIComponent(docId),
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ const typeOptions = [
|
||||
{ label: '计费', value: 'BILL' },
|
||||
{ label: '试算', value: 'QUOTE' },
|
||||
{ label: '扣减额度', value: 'CREDIT' },
|
||||
{ label: '欠费清除', value: 'ARREARS' }
|
||||
{ label: '欠费清除', value: 'ARREARS' },
|
||||
{ label: '订单数据源同步', value: 'ORDER_SOURCE_SYNC' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="520px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="!loading"
|
||||
:close-on-press-escape="!loading"
|
||||
:show-close="!loading"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<el-form v-show="!loading && !finished" label-width="96px">
|
||||
<el-form-item label="客户">
|
||||
<span>{{ filterText.customers }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="提货时间">
|
||||
<span>{{ filterText.time }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="filterText.nos" label="单号">
|
||||
<span>{{ filterText.nos }}</span>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="按当前筛选分页查询上游订单并逐单同步计费,避免一次性加载全量。"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<div v-if="loading || finished" class="filter-retry-progress">
|
||||
<el-progress
|
||||
:percentage="progressPercent"
|
||||
:status="progressStatus"
|
||||
:stroke-width="14"
|
||||
striped
|
||||
:striped-flow="loading"
|
||||
/>
|
||||
<div class="filter-retry-progress__msg">{{ progressMessage }}</div>
|
||||
<div class="filter-retry-progress__text">
|
||||
总数 {{ progress.total || 0 }}
|
||||
/ 已处理 {{ progress.processed || 0 }}
|
||||
/ 成功 {{ progress.ok || 0 }}
|
||||
/ 失败 {{ progress.fail || 0 }}
|
||||
</div>
|
||||
<div v-if="progress.error" class="filter-retry-progress__error">{{ progress.error }}</div>
|
||||
<p v-if="loading" class="filter-retry-progress__hint">请勿关闭页面,大量订单同步计费可能需要一些时间</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!loading && !finished">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">开始同步计费</el-button>
|
||||
</template>
|
||||
<template v-else-if="finished">
|
||||
<el-button type="primary" @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button disabled>执行中...</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { syncOrderSourceByFilter, getOrderSourceSyncProgress } from '@/api/monitor/orderSource'
|
||||
|
||||
const emit = defineEmits(['success', 'progress'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const query = ref({})
|
||||
const progress = ref({
|
||||
percent: 0,
|
||||
message: '',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: '',
|
||||
error: ''
|
||||
})
|
||||
|
||||
const filterText = computed(() => {
|
||||
const q = query.value || {}
|
||||
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(Boolean) : []
|
||||
const nos = []
|
||||
if (q.orderId) nos.push('订单号 ' + q.orderId)
|
||||
if (q.orgOrderId) nos.push('运单号 ' + q.orgOrderId)
|
||||
if (q.transferOrderId) nos.push('转单号 ' + q.transferOrderId)
|
||||
return {
|
||||
customers: codes.join(', ') || '-',
|
||||
time: (q.beginTime || '-') + ' ~ ' + (q.endTime || '-'),
|
||||
nos: nos.join(' / ')
|
||||
}
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (loading.value) return '异步同步计费进行中'
|
||||
if (finished.value) return '异步同步计费完成'
|
||||
return '条件批量同步计费'
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => Math.min(100, Math.max(0, Number(progress.value?.percent) || 0)))
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'exception'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const progressMessage = computed(() => mapProgressMessage(progress.value?.message, progress.value))
|
||||
|
||||
function mapProgressMessage(message, data) {
|
||||
const m = message || ''
|
||||
if (!m || m === 'queued') return '排队中...'
|
||||
if (m === 'starting') return '正在统计订单...'
|
||||
if (m === 'no tasks') return '无匹配订单'
|
||||
if (m === 'failed') return '执行失败'
|
||||
if (m.startsWith('processing ')) {
|
||||
return '正在同步计费 ' + m.slice('processing '.length)
|
||||
}
|
||||
if (m.startsWith('done ')) {
|
||||
return '完成:成功 ' + (data?.ok || 0) + ' / 失败 ' + (data?.fail || 0)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
function emitProgress() {
|
||||
emit('progress', {
|
||||
loading: loading.value,
|
||||
finished: finished.value,
|
||||
percent: progressPercent.value,
|
||||
status: progress.value?.status || '',
|
||||
message: progressMessage.value,
|
||||
total: progress.value?.total || 0,
|
||||
processed: progress.value?.processed || 0,
|
||||
ok: progress.value?.ok || 0,
|
||||
fail: progress.value?.fail || 0,
|
||||
error: progress.value?.error || ''
|
||||
})
|
||||
}
|
||||
|
||||
function open(filterQuery, autoStart) {
|
||||
if (loading.value || finished.value) {
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
query.value = { ...(filterQuery || {}) }
|
||||
progress.value = {
|
||||
percent: 0,
|
||||
message: '',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: '',
|
||||
error: ''
|
||||
}
|
||||
loading.value = false
|
||||
finished.value = false
|
||||
visible.value = true
|
||||
emitProgress()
|
||||
if (autoStart) {
|
||||
submit(true)
|
||||
}
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
if (!loading.value) {
|
||||
finished.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function submit(skipConfirm) {
|
||||
const q = query.value || {}
|
||||
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(v => v != null && String(v).trim() !== '') : []
|
||||
if (!codes.length || !q.beginTime || !q.endTime) {
|
||||
proxy.$modal.msgError('请先选择客户和提货时间')
|
||||
return
|
||||
}
|
||||
const start = () => {
|
||||
loading.value = true
|
||||
finished.value = false
|
||||
progress.value = {
|
||||
percent: 0,
|
||||
message: 'queued',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
ok: 0,
|
||||
fail: 0,
|
||||
status: 'PENDING',
|
||||
error: ''
|
||||
}
|
||||
emitProgress()
|
||||
return syncOrderSourceByFilter({
|
||||
customerCodes: codes,
|
||||
beginTime: q.beginTime,
|
||||
endTime: q.endTime,
|
||||
orderId: q.orderId || undefined,
|
||||
orgOrderId: q.orgOrderId || undefined,
|
||||
transferOrderId: q.transferOrderId || undefined,
|
||||
batchSize: 50
|
||||
}).then(res => {
|
||||
const asyncTaskId = res.data?.asyncTaskId || res.data
|
||||
if (!asyncTaskId) {
|
||||
throw new Error('asyncTaskId missing')
|
||||
}
|
||||
return poll(asyncTaskId)
|
||||
}).then((data) => {
|
||||
finished.value = true
|
||||
emit('success', data)
|
||||
proxy.$modal.msgSuccess(
|
||||
'完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0)
|
||||
)
|
||||
}).catch(() => {
|
||||
if (loading.value) {
|
||||
finished.value = true
|
||||
if (!progress.value?.status || progress.value.status === 'PENDING' || progress.value.status === 'RUNNING') {
|
||||
progress.value = {
|
||||
...progress.value,
|
||||
status: 'FAIL',
|
||||
percent: 100,
|
||||
message: 'failed',
|
||||
error: progress.value?.error || '批量同步计费失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
emitProgress()
|
||||
})
|
||||
}
|
||||
if (skipConfirm) {
|
||||
start()
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'确认按当前筛选分页查询并异步同步计费?',
|
||||
'二次确认'
|
||||
).then(() => start()).catch(() => {})
|
||||
}
|
||||
|
||||
function poll(asyncTaskId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
getOrderSourceSyncProgress(asyncTaskId).then(res => {
|
||||
const data = res.data || {}
|
||||
progress.value = {
|
||||
percent: Number(data.percent) || 0,
|
||||
message: data.message || '',
|
||||
total: data.total || 0,
|
||||
processed: data.processed || 0,
|
||||
ok: data.ok || 0,
|
||||
fail: data.fail || 0,
|
||||
status: data.status || '',
|
||||
error: data.error || ''
|
||||
}
|
||||
emitProgress()
|
||||
const st = (data.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') {
|
||||
progress.value.percent = 100
|
||||
resolve(data)
|
||||
return
|
||||
}
|
||||
if (st === 'FAIL') {
|
||||
proxy.$modal.msgError(data.error || data.message || '批量同步计费失败')
|
||||
reject(new Error(data.error || 'fail'))
|
||||
return
|
||||
}
|
||||
setTimeout(tick, 1500)
|
||||
}).catch(err => reject(err))
|
||||
}
|
||||
tick()
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-retry-progress {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.filter-retry-progress__msg {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.filter-retry-progress__text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.filter-retry-progress__error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.filter-retry-progress__hint {
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="860px" :title="'操作记录'" append-to-body destroy-on-close>
|
||||
<basic-table
|
||||
v-if="visible"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="docId"
|
||||
:page-size="10"
|
||||
:selection="false"
|
||||
:show-add="false"
|
||||
>
|
||||
<template #col-actionName="{ row }">
|
||||
{{ actionLabel(row) }}
|
||||
</template>
|
||||
<template #col-status="{ row }">
|
||||
<el-tag size="small" :type="row.status === 'FAIL' ? 'danger' : 'success'">{{ row.status === 'FAIL' ? '失败' : '成功' }}</el-tag>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-drawer v-model="detailOpen" size="640px" :title="'操作详情'" append-to-body destroy-on-close>
|
||||
<div v-loading="detailLoading">
|
||||
<el-empty v-if="!detail" description="暂无详情" />
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="small" :type="detail.status === 'FAIL' ? 'danger' : 'success'">{{ detail.status === 'FAIL' ? '失败' : '成功' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="写入时间" :span="2">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detail.errorMessage" label="错误信息" :span="2">
|
||||
<span class="err-text">{{ detail.errorMessage }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head"><span>请求报文</span></div>
|
||||
<pre class="code-pre">{{ formatJson(detail.requestJson) }}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head"><span>返回报文</span></div>
|
||||
<pre class="code-pre">{{ formatJson(detail.responseJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listOrderSourceSyncLog, getOrderSourceSyncLog } from '@/api/monitor/orderSource'
|
||||
|
||||
const visible = ref(false)
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '业务ID', prop: 'bizId', component: 'input', placeholder: '业务ID', width: '220px' },
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
component: 'select',
|
||||
options: [
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '失败', value: 'FAIL' }
|
||||
],
|
||||
placeholder: '请选择',
|
||||
width: '140px',
|
||||
props: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ label: '操作', prop: 'actionName', width: 120, slot: 'actionName' },
|
||||
{ label: '业务ID', prop: 'bizId', minWidth: 180, showOverflowTooltip: true },
|
||||
{ label: '状态', prop: 'status', align: 'center', width: 90, slot: 'status' },
|
||||
{ label: '写入时间', prop: 'updateTime', align: 'center', width: 170 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 80, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
function parseReq(row) {
|
||||
if (!row || !row.requestJson) return {}
|
||||
try {
|
||||
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function actionLabel(row) {
|
||||
const action = parseReq(row).action
|
||||
if (action === 'FILTER') return '筛选同步'
|
||||
if (action === 'FILTER_DONE') return '筛选完成'
|
||||
if (action === 'SELECTED') return '同步选中'
|
||||
if (action === 'ONE') return '单条同步'
|
||||
return action || '-'
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
return listOrderSourceSyncLog(params)
|
||||
}
|
||||
|
||||
function formatJson(raw) {
|
||||
if (raw == null || raw === '') return '-'
|
||||
try {
|
||||
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
return JSON.stringify(obj, null, 2)
|
||||
} catch (e) {
|
||||
return String(raw)
|
||||
}
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
detailOpen.value = true
|
||||
detail.value = null
|
||||
detailLoading.value = true
|
||||
getOrderSourceSyncLog(row.docId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
detailLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.err-text {
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.json-panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.json-panel__head {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.code-pre {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="app-container order-source-page">
|
||||
<div v-if="syncFloatVisible" class="retry-status-float">
|
||||
<el-tooltip :content="syncFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="primary" plain size="small" @click="openFilterSync()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': syncProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ syncProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ syncFloatTitle }}</strong>
|
||||
<small>{{ syncFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
@@ -7,6 +21,7 @@
|
||||
:query="queryList"
|
||||
row-key="orderId"
|
||||
:auto-load="false"
|
||||
selection
|
||||
search-action-align="center"
|
||||
:search-per-row="4"
|
||||
>
|
||||
@@ -21,6 +36,36 @@
|
||||
multiple
|
||||
/>
|
||||
</template>
|
||||
<template #toolbar="{ selection }">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Refresh"
|
||||
v-hasPermi="['monitor:orderSource:sync']"
|
||||
@click="openFilterSync()"
|
||||
>同步计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="Document"
|
||||
@click="logDrawerRef.open()"
|
||||
>操作记录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Refresh"
|
||||
:disabled="!selection.length"
|
||||
:loading="batchLoading"
|
||||
v-hasPermi="['monitor:orderSource:sync']"
|
||||
@click="handleBatchSync(selection)"
|
||||
>同步选中</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #col-product="{ row }">
|
||||
{{ row.productName || '-' }} ({{ row.productCode || '-' }})
|
||||
</template>
|
||||
@@ -30,15 +75,40 @@
|
||||
<template #col-orderStatus="{ row }">
|
||||
{{ orderStatusLabel(row.orderStatus) }}
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
icon="Refresh"
|
||||
:loading="syncingKey === rowKey(row)"
|
||||
v-hasPermi="['monitor:orderSource:sync']"
|
||||
@click="handleSyncOne(row)"
|
||||
>同步计费</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<order-source-sync-dialog
|
||||
ref="syncDialogRef"
|
||||
@progress="onSyncProgress"
|
||||
@success="onSyncSuccess"
|
||||
/>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OrderSource">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listOrderSource } from '@/api/monitor/orderSource'
|
||||
import { listOrderSource, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
|
||||
import OrderSourceSyncLogDrawer from './components/OrderSourceSyncLogDrawer.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const syncingKey = ref(null)
|
||||
const batchLoading = ref(false)
|
||||
const syncFloat = ref(null)
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '客户', prop: 'customerCodes', placeholder: '客户代码' },
|
||||
@@ -72,7 +142,8 @@ const columns = [
|
||||
{ label: '计费重', prop: 'dimensionalWeightFactor', align: 'center', width: 100 },
|
||||
{ label: '体积重', prop: 'volumeWeight', align: 'center', width: 100 },
|
||||
{ label: '实重', prop: 'weight', align: 'center', width: 100 },
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 }
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const orderStatusMap = {
|
||||
@@ -89,6 +160,25 @@ const orderStatusMap = {
|
||||
10: '已删除'
|
||||
}
|
||||
|
||||
const syncFloatVisible = computed(() => {
|
||||
const p = syncFloat.value
|
||||
return !!(p && (p.loading || p.finished))
|
||||
})
|
||||
const syncProgressDegree = computed(() => Math.round(((syncFloat.value?.percent || 0) / 100) * 360))
|
||||
const syncProgressText = computed(() => (syncFloat.value?.percent || 0) + '%')
|
||||
const syncFloatTitle = computed(() => {
|
||||
const st = (syncFloat.value?.status || '').toUpperCase()
|
||||
if (syncFloat.value?.loading) return '异步同步计费中'
|
||||
if (st === 'SUCCESS') return '同步计费完成'
|
||||
if (st === 'FAIL') return '同步计费失败'
|
||||
return '异步同步计费'
|
||||
})
|
||||
const syncFloatSummary = computed(() => {
|
||||
const p = syncFloat.value || {}
|
||||
return '成功 ' + (p.ok || 0) + ' / 失败 ' + (p.fail || 0) + ' / 总数 ' + (p.total || 0)
|
||||
})
|
||||
const syncFloatTooltip = computed(() => syncFloat.value?.message || '点击查看详情')
|
||||
|
||||
function orderStatusLabel(status) {
|
||||
if (status === null || status === undefined || status === '') return '-'
|
||||
const key = Number(status)
|
||||
@@ -98,6 +188,10 @@ function orderStatusLabel(status) {
|
||||
return String(status)
|
||||
}
|
||||
|
||||
function rowKey(row) {
|
||||
return (row && (row.orderId || row.transferOrderId || row.orgOrderId)) || ''
|
||||
}
|
||||
|
||||
function hasRequiredFilters(params) {
|
||||
const codes = params && params.customerCodes
|
||||
const hasCustomer = Array.isArray(codes) && codes.some(v => v != null && String(v).trim() !== '')
|
||||
@@ -106,17 +200,168 @@ function hasRequiredFilters(params) {
|
||||
return hasCustomer && hasTime
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
if (!hasRequiredFilters(params)) {
|
||||
ElMessage.warning('请先选择客户和提货时间')
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
const { dateRange, ...rest } = params
|
||||
function buildFilterQuery(params) {
|
||||
const src = params || tableRef.value?.getQueryParams?.() || {}
|
||||
const { dateRange, pageNum, pageSize, ...rest } = src
|
||||
const query = { ...rest }
|
||||
if (Array.isArray(dateRange) && dateRange[0] && dateRange[1]) {
|
||||
query.beginTime = dateRange[0]
|
||||
query.endTime = dateRange[1]
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
if (!hasRequiredFilters(params)) {
|
||||
ElMessage.warning('请先选择客户和提货时间')
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
const query = buildFilterQuery(params)
|
||||
query.pageNum = params.pageNum
|
||||
query.pageSize = params.pageSize
|
||||
return listOrderSource(query)
|
||||
}
|
||||
|
||||
function openFilterSync() {
|
||||
const q = buildFilterQuery()
|
||||
if (!q.customerCodes || !q.beginTime || !q.endTime) {
|
||||
ElMessage.warning('请先选择客户和提货时间')
|
||||
return
|
||||
}
|
||||
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(v => v != null && String(v).trim() !== '') : []
|
||||
if (!codes.length) {
|
||||
ElMessage.warning('请先选择客户和提货时间')
|
||||
return
|
||||
}
|
||||
q.customerCodes = codes
|
||||
proxy.$modal.confirm('确认按当前筛选分页查询并异步同步计费?', '二次确认').then(() => {
|
||||
syncDialogRef.value?.open(q, true)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function onSyncProgress(payload) {
|
||||
syncFloat.value = payload
|
||||
if (payload && payload.finished && !payload.loading) {
|
||||
window.setTimeout(() => {
|
||||
if (syncFloat.value && syncFloat.value.finished && !syncFloat.value.loading) {
|
||||
syncFloat.value = null
|
||||
}
|
||||
}, 8000)
|
||||
}
|
||||
}
|
||||
|
||||
function onSyncSuccess() {
|
||||
tableRef.value?.refresh()
|
||||
}
|
||||
|
||||
function toSyncRow(row) {
|
||||
return {
|
||||
orderId: row.orderId,
|
||||
orgOrderId: row.orgOrderId,
|
||||
transferOrderId: row.transferOrderId,
|
||||
customerCode: row.customerCode,
|
||||
pickupDate: row.pickupDate
|
||||
}
|
||||
}
|
||||
|
||||
function handleSyncOne(row) {
|
||||
proxy.$modal.confirm('确认同步计费该订单?', '二次确认').then(() => {
|
||||
syncingKey.value = rowKey(row)
|
||||
return syncOrderSourceSelected([toSyncRow(row)])
|
||||
}).then(res => {
|
||||
const data = res.data || {}
|
||||
if ((data.ok || 0) > 0 && !(data.fail || 0)) {
|
||||
proxy.$modal.msgSuccess('同步成功')
|
||||
} else {
|
||||
proxy.$modal.msgError((data.details && data.details[0] && data.details[0].errorMessage) || '同步失败')
|
||||
}
|
||||
tableRef.value.refresh()
|
||||
}).catch(() => {}).finally(() => {
|
||||
syncingKey.value = null
|
||||
})
|
||||
}
|
||||
|
||||
function handleBatchSync(selection) {
|
||||
if (!selection || !selection.length) return
|
||||
proxy.$modal.confirm('确认同步选中的 ' + selection.length + ' 条订单?', '二次确认').then(() => {
|
||||
batchLoading.value = true
|
||||
return syncOrderSourceSelected(selection.map(toSyncRow))
|
||||
}).then(res => {
|
||||
const data = res.data || {}
|
||||
proxy.$modal.msgSuccess('完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0))
|
||||
tableRef.value.refresh()
|
||||
}).catch(() => {}).finally(() => {
|
||||
batchLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-source-page .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 24px;
|
||||
z-index: 3001;
|
||||
}
|
||||
|
||||
.order-source-page .retry-status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 68px;
|
||||
padding: 6px 16px 6px 7px;
|
||||
border-radius: 36px;
|
||||
}
|
||||
|
||||
.order-source-page .retry-progress-ring {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
|
||||
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
|
||||
);
|
||||
}
|
||||
|
||||
.order-source-page .retry-progress-ring::after {
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.order-source-page .retry-progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.order-source-page .retry-status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-source-page .retry-status-text strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.order-source-page .retry-status-text small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user