Merge branch 'master' of http://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
+10
-1
@@ -194,6 +194,15 @@
|
|||||||
<source>${java.version}</source>
|
<source>${java.version}</source>
|
||||||
<target>${java.version}</target>
|
<target>${java.version}</target>
|
||||||
<encoding>${project.build.sourceEncoding}</encoding>
|
<encoding>${project.build.sourceEncoding}</encoding>
|
||||||
|
<!-- JDK 23+ 默认不再自动发现注解处理器,显式启用 Lombok,避免 @Data 方法未生成。 -->
|
||||||
|
<proc>full</proc>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.46</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
@@ -229,4 +238,4 @@
|
|||||||
</pluginRepository>
|
</pluginRepository>
|
||||||
</pluginRepositories>
|
</pluginRepositories>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
+139
-32
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.web.controller.ar;
|
package com.ruoyi.web.controller.ar;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -17,7 +18,9 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.ar.domain.ArBill;
|
import com.ruoyi.ar.domain.ArBill;
|
||||||
import com.ruoyi.ar.domain.ArBankAccount;
|
import com.ruoyi.ar.domain.ArBankAccount;
|
||||||
|
import com.ruoyi.ar.domain.ArBillDetailFee;
|
||||||
import com.ruoyi.ar.domain.ArBillExport;
|
import com.ruoyi.ar.domain.ArBillExport;
|
||||||
|
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||||
import com.ruoyi.ar.domain.ArBillLog;
|
import com.ruoyi.ar.domain.ArBillLog;
|
||||||
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
||||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||||
@@ -33,114 +36,218 @@ import com.ruoyi.common.utils.StringUtils;
|
|||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||||
import com.ruoyi.price.domain.PrcOrderBill;
|
import com.ruoyi.price.domain.PrcOrderBill;
|
||||||
|
|
||||||
/** 应收账单管理。 */
|
/**
|
||||||
|
* 应收账单 HTTP 接口层。
|
||||||
|
*
|
||||||
|
* <p>负责账单列表、账单生成、确认/作废、异步导出、日志和详情分页接口;
|
||||||
|
* 具体业务规则统一下沉到 {@code IArBillService}。</p>
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/ar/bill")
|
@RequestMapping("/ar/bill")
|
||||||
@Validated
|
@Validated
|
||||||
public class ArBillController extends BaseController
|
public class ArBillController extends BaseController {
|
||||||
{
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IArBillService billService;
|
private IArBillService billService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账单列表;分页参数由 BaseController.startPage 交给 MyBatis PageHelper 处理。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(ArBillQuery query)
|
public TableDataInfo list(ArBillQuery query) {
|
||||||
{
|
|
||||||
startPage();
|
startPage();
|
||||||
List<ArBill> list = billService.selectArBillList(query);
|
List<ArBill> list = billService.selectArBillList(query);
|
||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账单头信息;账单明细通过独立分页接口按需加载。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
@GetMapping("/{arBillId}")
|
@GetMapping("/{arBillId}")
|
||||||
public AjaxResult getInfo(@PathVariable Long arBillId)
|
public AjaxResult getInfo(@PathVariable Long arBillId) {
|
||||||
{
|
|
||||||
return success(billService.selectArBillById(arBillId));
|
return success(billService.selectArBillById(arBillId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询指定币种的公司默认收款账户,账号返回脱敏值。 */
|
/**
|
||||||
|
* 分页查询账单明细,避免详情页一次性加载全部订单。
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
|
@GetMapping("/{arBillId}/details")
|
||||||
|
public TableDataInfo details(@PathVariable Long arBillId) {
|
||||||
|
startPage();
|
||||||
|
return getDataTable(billService.selectArBillDetails(arBillId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询单条账单明细的费用明细,按展开行懒加载。
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
|
@GetMapping("/{arBillId}/details/{detailId}/fees")
|
||||||
|
public AjaxResult detailFees(@PathVariable Long arBillId, @PathVariable Long detailId) {
|
||||||
|
List<ArBillDetailFee> fees = billService.selectArBillDetailFees(arBillId, detailId);
|
||||||
|
return success(fees);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定币种的公司默认收款账户,账号返回脱敏值。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
@GetMapping("/default-bank-account")
|
@GetMapping("/default-bank-account")
|
||||||
public AjaxResult defaultBankAccount(@RequestParam(required = false, defaultValue = "CNY") String currency)
|
public AjaxResult defaultBankAccount(@RequestParam(required = false, defaultValue = "CNY") String currency) {
|
||||||
{
|
|
||||||
ArBankAccount account = billService.getDefaultBankAccount(currency);
|
ArBankAccount account = billService.getDefaultBankAccount(currency);
|
||||||
return success(account);
|
return success(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询生成账单前的订单计费结果。 */
|
/**
|
||||||
|
* 查询生成账单前的订单计费结果。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
@GetMapping("/generate/preview")
|
@GetMapping("/generate/preview")
|
||||||
public TableDataInfo preview(ArBillGenerateQuery query)
|
public AjaxResult preview(ArBillGenerateQuery query) {
|
||||||
{
|
startPage();
|
||||||
List<PrcOrderBill> list = billService.previewBill(query);
|
List<PrcOrderBill> list = billService.previewBill(query);
|
||||||
return getDataTable(list);
|
TableDataInfo pageData = getDataTable(list);
|
||||||
|
AjaxResult result = success();
|
||||||
|
result.put("rows", pageData.getRows());
|
||||||
|
result.put("total", pageData.getTotal());
|
||||||
|
result.put("totalAmount", billService.sumPreviewBill(query));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建账单头和异步生成任务,接口不等待全部订单明细处理完成。
|
||||||
|
*/
|
||||||
@Log(title = "应收账单", businessType = BusinessType.INSERT)
|
@Log(title = "应收账单", businessType = BusinessType.INSERT)
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
@PostMapping("/generate")
|
@PostMapping("/generate")
|
||||||
public AjaxResult generate(@Valid @RequestBody ArBillGenerateRequest request,
|
public AjaxResult generate(@Valid @RequestBody ArBillGenerateRequest request,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
{
|
|
||||||
String requestId = requestId(request.getRequestId(), headerRequestId);
|
String requestId = requestId(request.getRequestId(), headerRequestId);
|
||||||
ArBill bill = billService.generateBill(request, getUsername(), requestId);
|
ArBillGenerateTask task = billService.generateBill(request, getUsername(), requestId);
|
||||||
return success(bill).put("request_id", requestId);
|
return success(task).put("request_id", requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询单个应收账单异步生成任务。
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
|
@GetMapping("/generate/task/{taskId}")
|
||||||
|
public AjaxResult generateTaskInfo(@PathVariable Long taskId) {
|
||||||
|
return success(billService.selectGenerateTask(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户仍在处理中的应收账单生成任务。
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
|
@GetMapping("/generate/tasks/active")
|
||||||
|
public AjaxResult activeGenerateTasks() {
|
||||||
|
return success(billService.selectActiveGenerateTasks(getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求取消应收账单异步生成任务。
|
||||||
|
*/
|
||||||
|
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
|
@PostMapping("/generate/task/{taskId}/cancel")
|
||||||
|
public AjaxResult cancelGenerateTask(@PathVariable Long taskId,
|
||||||
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
|
String requestId = requestId(null, headerRequestId);
|
||||||
|
return success(billService.cancelGenerateTask(taskId, getUsername(), requestId)).put("request_id", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整单重新发起应收账单异步生成任务。
|
||||||
|
*/
|
||||||
|
@Log(title = "应收账单", businessType = BusinessType.INSERT)
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||||
|
@PostMapping("/generate/task/{taskId}/retry")
|
||||||
|
public AjaxResult retryGenerateTask(@PathVariable Long taskId,
|
||||||
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
|
String requestId = requestId(null, headerRequestId);
|
||||||
|
return success(billService.retryGenerateTask(taskId, getUsername(), requestId)).put("request_id", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认已生成完成的账单,使其进入可核销状态。
|
||||||
|
*/
|
||||||
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
|
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:confirm')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:confirm')")
|
||||||
@PutMapping("/{arBillId}/confirm")
|
@PutMapping("/{arBillId}/confirm")
|
||||||
public AjaxResult confirm(@PathVariable Long arBillId, @RequestBody(required = false) ArBillActionRequest request,
|
public AjaxResult confirm(@PathVariable Long arBillId, @RequestBody(required = false) ArBillActionRequest request,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
{
|
|
||||||
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
||||||
return success(billService.confirmBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
|
return success(billService.confirmBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作废尚未发生核销的账单;账单明细保留作为历史快照。
|
||||||
|
*/
|
||||||
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
|
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:void')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:void')")
|
||||||
@PutMapping("/{arBillId}/void")
|
@PutMapping("/{arBillId}/void")
|
||||||
public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request,
|
public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
{
|
|
||||||
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId);
|
||||||
return success(billService.voidBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
|
return success(billService.voidBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅提交导出任务;前端通过导出任务状态接口轮询并在完成后下载。
|
||||||
|
*/
|
||||||
@Log(title = "应收账单导出", businessType = BusinessType.EXPORT)
|
@Log(title = "应收账单导出", businessType = BusinessType.EXPORT)
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:export')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:export')")
|
||||||
@PostMapping("/{arBillId}/export")
|
@PostMapping("/{arBillId}/export")
|
||||||
public AjaxResult export(@PathVariable Long arBillId,
|
public AjaxResult export(@PathVariable Long arBillId,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId)
|
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) {
|
||||||
{
|
|
||||||
String requestId = requestId(null, headerRequestId);
|
String requestId = requestId(null, headerRequestId);
|
||||||
return success(billService.exportBill(arBillId, getUsername(), requestId)).put("request_id", requestId);
|
return success(billService.exportBill(arBillId, getUsername(), requestId)).put("request_id", requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前账单的导出历史和任务状态。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
@GetMapping("/{arBillId}/exports")
|
@GetMapping("/{arBillId}/exports")
|
||||||
public AjaxResult exports(@PathVariable Long arBillId)
|
public AjaxResult exports(@PathVariable Long arBillId) {
|
||||||
{
|
|
||||||
return success(billService.selectExports(arBillId));
|
return success(billService.selectExports(arBillId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询单个账单导出任务状态,供前端轮询。
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
|
||||||
|
@GetMapping("/export/{exportId}")
|
||||||
|
public AjaxResult exportInfo(@PathVariable Long exportId) {
|
||||||
|
return success(billService.selectExport(exportId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载已成功生成的导出文件;文件内容由服务层从临时文件或配置存储读取。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:bill:export')")
|
@PreAuthorize("@ss.hasPermi('ar:bill:export')")
|
||||||
@GetMapping("/export/{exportId}/download")
|
@GetMapping("/export/{exportId}/download")
|
||||||
public void download(@PathVariable Long exportId, HttpServletResponse response) throws Exception
|
public void download(@PathVariable Long exportId, HttpServletResponse response) throws Exception {
|
||||||
{
|
|
||||||
billService.downloadExport(exportId, response);
|
billService.downloadExport(exportId, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询账单操作日志。
|
||||||
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('ar:log:list')")
|
@PreAuthorize("@ss.hasPermi('ar:log:list')")
|
||||||
@GetMapping("/log/list")
|
@GetMapping("/log/list")
|
||||||
public TableDataInfo logs(ArBillLog query)
|
public TableDataInfo logs(ArBillLog query) {
|
||||||
{
|
|
||||||
startPage();
|
startPage();
|
||||||
return getDataTable(billService.selectLogList(query));
|
return getDataTable(billService.selectLogList(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String requestId(String bodyRequestId, String headerRequestId)
|
/**
|
||||||
{
|
* 解析接口幂等号:优先使用请求体,再使用请求头,最后生成新的 UUID。
|
||||||
|
* 生成账单、确认、作废和导出都依赖该值避免重复提交。
|
||||||
|
*/
|
||||||
|
private String requestId(String bodyRequestId, String headerRequestId) {
|
||||||
if (StringUtils.isNotEmpty(bodyRequestId)) return bodyRequestId;
|
if (StringUtils.isNotEmpty(bodyRequestId)) return bodyRequestId;
|
||||||
if (StringUtils.isNotEmpty(headerRequestId)) return headerRequestId;
|
if (StringUtils.isNotEmpty(headerRequestId)) return headerRequestId;
|
||||||
return IdUtils.fastSimpleUUID();
|
return IdUtils.fastSimpleUUID();
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ spring:
|
|||||||
connectTimeout: 30000
|
connectTimeout: 30000
|
||||||
# 配置网络超时时间
|
# 配置网络超时时间
|
||||||
socketTimeout: 60000
|
socketTimeout: 60000
|
||||||
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
|
# 配置间隔多久检测一次空闲连接,避免远程数据库断开后连接池继续复用失效连接,单位是毫秒
|
||||||
timeBetweenEvictionRunsMillis: 60000
|
timeBetweenEvictionRunsMillis: 30000
|
||||||
# 配置一个连接在池中最小生存的时间,单位是毫秒
|
# 配置一个连接在池中最小生存的时间,单位是毫秒
|
||||||
minEvictableIdleTimeMillis: 300000
|
minEvictableIdleTimeMillis: 300000
|
||||||
# 配置一个连接在池中最大生存的时间,单位是毫秒
|
# 配置一个连接在池中最大生存的时间,单位是毫秒
|
||||||
@@ -38,7 +38,8 @@ spring:
|
|||||||
# 配置检测连接是否有效
|
# 配置检测连接是否有效
|
||||||
validationQuery: SELECT 1 FROM DUAL
|
validationQuery: SELECT 1 FROM DUAL
|
||||||
testWhileIdle: true
|
testWhileIdle: true
|
||||||
testOnBorrow: false
|
# 异步账单任务借用连接时校验连接有效性,避免拿到已被数据库断开的连接
|
||||||
|
testOnBorrow: true
|
||||||
testOnReturn: false
|
testOnReturn: false
|
||||||
webStatFilter:
|
webStatFilter:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -59,4 +60,4 @@ spring:
|
|||||||
merge-sql: true
|
merge-sql: true
|
||||||
wall:
|
wall:
|
||||||
config:
|
config:
|
||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
|
|||||||
@@ -26,9 +26,11 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
<!-- Java 26 编译环境使用新版 Lombok,确保模型类的 getter/setter 正常生成。 -->
|
||||||
|
<version>1.18.46</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.ar.config;
|
||||||
|
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
/** 应收账单大文件导出专用线程池。 */
|
||||||
|
@Configuration
|
||||||
|
public class ArBillExportTaskConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 账单导出会同时占用数据库连接、CPU 和磁盘,最多允许两个任务并发生成。
|
||||||
|
* 队列满时由提交器将任务标记为失败,避免回退到 Web 请求线程执行。
|
||||||
|
*/
|
||||||
|
@Bean(name = "arBillExportTaskExecutor")
|
||||||
|
public ThreadPoolTaskExecutor arBillExportTaskExecutor()
|
||||||
|
{
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(2);
|
||||||
|
executor.setQueueCapacity(2);
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
executor.setThreadNamePrefix("ar-bill-export-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单生成会同时占用数据库连接、CPU 和磁盘,2核4G环境下先限制为单任务串行处理。
|
||||||
|
*/
|
||||||
|
@Bean(name = "arBillGenerateTaskExecutor")
|
||||||
|
public ThreadPoolTaskExecutor arBillGenerateTaskExecutor()
|
||||||
|
{
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(1);
|
||||||
|
executor.setMaxPoolSize(1);
|
||||||
|
executor.setQueueCapacity(5);
|
||||||
|
executor.setKeepAliveSeconds(60);
|
||||||
|
executor.setThreadNamePrefix("ar-bill-generate-");
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,15 @@ package com.ruoyi.ar.domain;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 公司收款账户配置。 */
|
/**
|
||||||
|
* 公司收款账户配置。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ArBankAccount implements Serializable
|
public class ArBankAccount implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long bankAccountId;
|
private Long bankAccountId;
|
||||||
|
|||||||
@@ -3,41 +3,72 @@ package com.ruoyi.ar.domain;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import com.ruoyi.common.core.domain.BaseEntity;
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
/** 正式应收账单。 */
|
/**
|
||||||
|
* 正式应收账单。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class ArBill extends BaseEntity
|
public class ArBill extends BaseEntity {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单头主键。
|
||||||
|
*/
|
||||||
private Long arBillId;
|
private Long arBillId;
|
||||||
|
/**
|
||||||
|
* 对外展示的账单编号。
|
||||||
|
*/
|
||||||
private String billNo;
|
private String billNo;
|
||||||
|
/**
|
||||||
|
* 财务填写的账单名称。
|
||||||
|
*/
|
||||||
private String billName;
|
private String billName;
|
||||||
|
/**
|
||||||
|
* 账单所属客户主键。
|
||||||
|
*/
|
||||||
private Long customerId;
|
private Long customerId;
|
||||||
private String customerCode;
|
private String customerCode;
|
||||||
private String customerName;
|
private String customerName;
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
/** 账单所属期开始日期,导出模板展示用。 */
|
||||||
private Date billPeriodStart;
|
private Date billPeriodStart;
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
/** 账单所属期结束日期,同时作为生成订单的截止日期。 */
|
||||||
private Date billPeriodEnd;
|
private Date billPeriodEnd;
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date billDate;
|
private Date billDate;
|
||||||
private String currency;
|
private String currency;
|
||||||
private Integer orderCount;
|
private Integer orderCount;
|
||||||
|
/**
|
||||||
|
* 账单明细应收金额合计。
|
||||||
|
*/
|
||||||
private BigDecimal totalAmount;
|
private BigDecimal totalAmount;
|
||||||
|
/**
|
||||||
|
* 已通过余额核销的金额。
|
||||||
|
*/
|
||||||
private BigDecimal writtenOffAmount;
|
private BigDecimal writtenOffAmount;
|
||||||
|
/**
|
||||||
|
* 尚未核销的金额,核销入口按此字段判断是否可操作。
|
||||||
|
*/
|
||||||
private BigDecimal unwrittenAmount;
|
private BigDecimal unwrittenAmount;
|
||||||
/** 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。 */
|
/**
|
||||||
|
* 状态:DRAFT草稿、CONFIRMED已确认、PARTIAL_WRITTEN_OFF部分核销、WRITTEN_OFF已核销、VOID已作废。
|
||||||
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
/**
|
||||||
|
* 生成状态:GENERATING生成中、READY可用、FAILED生成失败。
|
||||||
|
*/
|
||||||
|
private String generationStatus;
|
||||||
private Integer billVersion;
|
private Integer billVersion;
|
||||||
private String bankAccountName;
|
private String bankAccountName;
|
||||||
private String bankAccountNo;
|
private String bankAccountNo;
|
||||||
@@ -47,7 +78,16 @@ public class ArBill extends BaseEntity
|
|||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date confirmedTime;
|
private Date confirmedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单明细;详情接口分页查询时通常不直接填充该集合。
|
||||||
|
*/
|
||||||
private List<ArBillDetail> details;
|
private List<ArBillDetail> details;
|
||||||
|
/**
|
||||||
|
* 当前账单的导出历史。
|
||||||
|
*/
|
||||||
private List<ArBillExport> exports;
|
private List<ArBillExport> exports;
|
||||||
|
/**
|
||||||
|
* 当前账单的操作日志。
|
||||||
|
*/
|
||||||
private List<ArBillLog> logs;
|
private List<ArBillLog> logs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import java.io.Serializable;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 账单订单快照。 */
|
/**
|
||||||
|
* 账单订单快照。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ArBillDetail implements Serializable
|
public class ArBillDetail implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long detailId;
|
private Long detailId;
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ package com.ruoyi.ar.domain;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 账单费用快照。 */
|
/**
|
||||||
|
* 账单费用快照。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ArBillDetailFee implements Serializable
|
public class ArBillDetailFee implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long detailFeeId;
|
private Long detailFeeId;
|
||||||
|
|||||||
@@ -2,26 +2,49 @@ package com.ruoyi.ar.domain;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 账单导出文件元数据。 */
|
/**
|
||||||
|
* 账单导出文件元数据。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ArBillExport implements Serializable
|
public class ArBillExport implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long exportId;
|
private Long exportId;
|
||||||
private Long arBillId;
|
private Long arBillId;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private String fileType;
|
private String fileType;
|
||||||
|
/**
|
||||||
|
* 提交导出任务时绑定的模板版本,避免后续模板变更影响历史任务。
|
||||||
|
*/
|
||||||
|
private Long templateId;
|
||||||
private String templateCode;
|
private String templateCode;
|
||||||
private String templateVersion;
|
private String templateVersion;
|
||||||
|
private String templateStorageType;
|
||||||
|
private String templateStorageKey;
|
||||||
|
private String templateHash;
|
||||||
|
/**
|
||||||
|
* 生成文件的实际存储键,下载接口据此读取文件。
|
||||||
|
*/
|
||||||
private String storageKey;
|
private String storageKey;
|
||||||
|
/**
|
||||||
|
* 文件字节数,便于前端展示和运维排查。
|
||||||
|
*/
|
||||||
private Long fileSize;
|
private Long fileSize;
|
||||||
|
/**
|
||||||
|
* 文件摘要,用于校验文件完整性。
|
||||||
|
*/
|
||||||
private String fileHash;
|
private String fileHash;
|
||||||
|
/**
|
||||||
|
* 导出的明细行数。
|
||||||
|
*/
|
||||||
private Integer rowCount;
|
private Integer rowCount;
|
||||||
/** 导出状态:PROCESSING生成中、SUCCESS成功可下载、FAILED失败。 */
|
/**
|
||||||
|
* 导出状态:PROCESSING生成中、SUCCESS成功可下载、FAILED失败。
|
||||||
|
*/
|
||||||
private String exportStatus;
|
private String exportStatus;
|
||||||
private String exportBy;
|
private String exportBy;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ruoyi.ar.domain;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单异步生成任务。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArBillGenerateTask implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步任务主键。
|
||||||
|
*/
|
||||||
|
private Long taskId;
|
||||||
|
/**
|
||||||
|
* 面向日志和前端展示的任务编号。
|
||||||
|
*/
|
||||||
|
private String taskNo;
|
||||||
|
/**
|
||||||
|
* 任务对应的账单头主键。
|
||||||
|
*/
|
||||||
|
private Long arBillId;
|
||||||
|
/**
|
||||||
|
* 订单选择模式 code:EXPLICIT=手动选中,ALL_MATCHED=全选查询结果。
|
||||||
|
*/
|
||||||
|
private String selectionMode;
|
||||||
|
/**
|
||||||
|
* 生成请求快照,避免任务执行过程中依赖前端当前条件。
|
||||||
|
*/
|
||||||
|
private String requestPayload;
|
||||||
|
/**
|
||||||
|
* 任务创建时的来源订单创建时间快照。
|
||||||
|
*/
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date snapshotCreateTime;
|
||||||
|
/**
|
||||||
|
* 按 bill_id 字典序处理的游标。
|
||||||
|
*/
|
||||||
|
private String lastSourceBillId;
|
||||||
|
/**
|
||||||
|
* 待处理总数;全选模式统计完成前使用 -1。
|
||||||
|
*/
|
||||||
|
private Integer totalCount;
|
||||||
|
/**
|
||||||
|
* 已扫描的来源订单数,用于进度展示。
|
||||||
|
*/
|
||||||
|
private Integer processedCount;
|
||||||
|
/**
|
||||||
|
* 成功写入账单明细的订单数。
|
||||||
|
*/
|
||||||
|
private Integer successCount;
|
||||||
|
/**
|
||||||
|
* 已写入账单明细的累计应收金额。
|
||||||
|
*/
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
/**
|
||||||
|
* 任务状态,取 ArBillGenerateTaskStatus 中定义的值。
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
/**
|
||||||
|
* 失败、取消或补偿清理时返回给用户的说明。
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
/**
|
||||||
|
* 请求幂等号,防止重复创建账单生成任务。
|
||||||
|
*/
|
||||||
|
private String requestToken;
|
||||||
|
/**
|
||||||
|
* 发起任务的用户。
|
||||||
|
*/
|
||||||
|
private String createBy;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date createTime;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
/** 首次被执行器领取的时间。 */
|
||||||
|
private Date startedAt;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
/** 最近一次进度更新时间,用于识别执行节点异常。 */
|
||||||
|
private Date heartbeatTime;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
/** 成功、失败或取消完成的时间。 */
|
||||||
|
private Date finishedAt;
|
||||||
|
}
|
||||||
@@ -2,23 +2,31 @@ package com.ruoyi.ar.domain;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 应收模块追加式审计日志。 */
|
/**
|
||||||
|
* 应收模块追加式审计日志。
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ArBillLog implements Serializable
|
public class ArBillLog implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long logId;
|
private Long logId;
|
||||||
private Long arBillId;
|
private Long arBillId;
|
||||||
/** 对象类型:BILL应收账单、EXPORT账单导出、BALANCE_WRITEOFF余额核销。 */
|
/**
|
||||||
|
* 对象类型:BILL应收账单、EXPORT账单导出、BALANCE_WRITEOFF余额核销。
|
||||||
|
*/
|
||||||
private String objectType;
|
private String objectType;
|
||||||
private String objectId;
|
private String objectId;
|
||||||
/** 对象业务标识,例如账单号、收款单号或核销单号。 */
|
/**
|
||||||
|
* 对象业务标识,例如账单号、收款单号或核销单号。
|
||||||
|
*/
|
||||||
private String objectDisplay;
|
private String objectDisplay;
|
||||||
/** 操作类型,取值由 ArLogOperationType 维护。 */
|
/**
|
||||||
|
* 操作类型,取值由 ArLogOperationType 维护。
|
||||||
|
*/
|
||||||
private String operationType;
|
private String operationType;
|
||||||
private String operationDesc;
|
private String operationDesc;
|
||||||
private String fromStatus;
|
private String fromStatus;
|
||||||
@@ -27,11 +35,15 @@ public class ArBillLog implements Serializable
|
|||||||
private String afterData;
|
private String afterData;
|
||||||
private String operatorId;
|
private String operatorId;
|
||||||
private String operatorName;
|
private String operatorName;
|
||||||
/** 操作来源:WEB网页端、API接口、JOB定时任务、IMPORT数据导入。 */
|
/**
|
||||||
|
* 操作来源:WEB网页端、API接口、JOB定时任务、IMPORT数据导入。
|
||||||
|
*/
|
||||||
private String operationSource;
|
private String operationSource;
|
||||||
private String requestId;
|
private String requestId;
|
||||||
private String traceId;
|
private String traceId;
|
||||||
/** 操作结果:SUCCESS成功、FAILED失败。 */
|
/**
|
||||||
|
* 操作结果:SUCCESS成功、FAILED失败。
|
||||||
|
*/
|
||||||
private String result;
|
private String result;
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
private String clientIp;
|
private String clientIp;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ruoyi.ar.domain;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单导出模板元数据。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ArExportTemplate implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long templateId;
|
||||||
|
private String templateCode;
|
||||||
|
private String templateName;
|
||||||
|
private String businessType;
|
||||||
|
private String templateVersion;
|
||||||
|
private String storageType;
|
||||||
|
private String storageKey;
|
||||||
|
private String fileHash;
|
||||||
|
private String status;
|
||||||
|
private String isDefault;
|
||||||
|
private Date effectiveStart;
|
||||||
|
private Date effectiveEnd;
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -16,4 +16,10 @@ public class ArBillGenerateQuery
|
|||||||
private Date billPeriodEnd;
|
private Date billPeriodEnd;
|
||||||
|
|
||||||
private String currency;
|
private String currency;
|
||||||
|
|
||||||
|
/** 订单号或运单号关键词,按 OR 条件模糊查询。 */
|
||||||
|
private String orderOrTrackingNo;
|
||||||
|
|
||||||
|
/** 全选模式下,排除弹窗使用的附加订单号或运单号关键词。 */
|
||||||
|
private String excludeOrderOrTrackingNo;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -4,8 +4,8 @@ import java.util.Date;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import com.ruoyi.ar.enums.ArBillSelectionMode;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/** 从订单计费结果生成正式账单的参数。 */
|
/** 从订单计费结果生成正式账单的参数。 */
|
||||||
@@ -21,14 +21,21 @@ public class ArBillGenerateRequest
|
|||||||
@NotNull(message = "账单截止日不能为空")
|
@NotNull(message = "账单截止日不能为空")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date billPeriodEnd;
|
private Date billPeriodEnd;
|
||||||
@NotEmpty(message = "请至少选择一条订单")
|
/** 手动模式选中的来源订单 ID;全选模式为空。 */
|
||||||
private List<String> sourceBillIds;
|
private List<String> sourceBillIds;
|
||||||
|
/** 选择模式:EXPLICIT 手动选择,ALL_MATCHED 全选查询结果。 */
|
||||||
|
private String selectionMode = ArBillSelectionMode.EXPLICIT.getCode();
|
||||||
|
/** 全选模式中用户明确排除的来源订单 ID。 */
|
||||||
|
private List<String> excludedSourceBillIds;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
private Date billDate;
|
private Date billDate;
|
||||||
private String currency = "CNY";
|
private String currency = "CNY";
|
||||||
|
/** 订单号或运单号关键词,生成时用于再次校验筛选条件。 */
|
||||||
|
private String orderOrTrackingNo;
|
||||||
private String bankAccountName;
|
private String bankAccountName;
|
||||||
private String bankAccountNo;
|
private String bankAccountNo;
|
||||||
private String bankName;
|
private String bankName;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
/** 前端幂等号;重复提交同一请求时复用原任务。 */
|
||||||
private String requestId;
|
private String requestId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ruoyi.ar.enums;
|
||||||
|
|
||||||
|
/** 应收账单异步生成任务状态。 */
|
||||||
|
public final class ArBillGenerateTaskStatus
|
||||||
|
{
|
||||||
|
/** 待线程池领取的任务。 */
|
||||||
|
public static final String WAITING = "WAITING";
|
||||||
|
/** 已被某个执行节点领取,正在生成明细。 */
|
||||||
|
public static final String RUNNING = "RUNNING";
|
||||||
|
/** 明细全部生成成功。 */
|
||||||
|
public static final String SUCCESS = "SUCCESS";
|
||||||
|
/** 生成失败,账单已进入失败状态。 */
|
||||||
|
public static final String FAILED = "FAILED";
|
||||||
|
/** 用户已请求取消,执行器会在安全边界清理已生成数据。 */
|
||||||
|
public static final String CANCEL_REQUESTED = "CANCEL_REQUESTED";
|
||||||
|
/** 取消清理完成,账单及其生成明细已回收。 */
|
||||||
|
public static final String CANCELED = "CANCELED";
|
||||||
|
|
||||||
|
private ArBillGenerateTaskStatus()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ruoyi.ar.enums;
|
||||||
|
|
||||||
|
/** 应收账单生成状态,与账单业务状态分离。 */
|
||||||
|
public final class ArBillGenerationStatus
|
||||||
|
{
|
||||||
|
/** 账单头已创建,明细和费用正在异步生成。 */
|
||||||
|
public static final String GENERATING = "GENERATING";
|
||||||
|
/** 明细生成完成,允许确认、核销和导出。 */
|
||||||
|
public static final String READY = "READY";
|
||||||
|
/** 明细生成失败或取消后已完成清理。 */
|
||||||
|
public static final String FAILED = "FAILED";
|
||||||
|
|
||||||
|
private ArBillGenerationStatus()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ruoyi.ar.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单生成时的订单选择模式。
|
||||||
|
* 数据库和接口仍保存英文 code,业务代码通过枚举统一校验和判断。
|
||||||
|
*/
|
||||||
|
public enum ArBillSelectionMode
|
||||||
|
{
|
||||||
|
/** 手动选择当前预览结果中的订单。 */
|
||||||
|
EXPLICIT("EXPLICIT", "手动选择"),
|
||||||
|
/** 选择全部查询结果,仅排除用户明确勾选的订单。 */
|
||||||
|
ALL_MATCHED("ALL_MATCHED", "全选查询结果");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
ArBillSelectionMode(String code, String label)
|
||||||
|
{
|
||||||
|
this.code = code;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode()
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空值按历史默认行为视为手动选择,非法非空值返回 null 交由业务层记录失败日志。 */
|
||||||
|
public static ArBillSelectionMode of(String code)
|
||||||
|
{
|
||||||
|
if (code == null || code.trim().isEmpty())
|
||||||
|
{
|
||||||
|
return EXPLICIT;
|
||||||
|
}
|
||||||
|
for (ArBillSelectionMode mode : values())
|
||||||
|
{
|
||||||
|
if (mode.code.equalsIgnoreCase(code.trim()))
|
||||||
|
{
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,16 +28,19 @@ public enum ArBillStatus
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 只有草稿账单允许确认。 */
|
||||||
public boolean canConfirm()
|
public boolean canConfirm()
|
||||||
{
|
{
|
||||||
return this == DRAFT;
|
return this == DRAFT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 草稿或已确认账单允许作废;已核销账单不可直接作废。 */
|
||||||
public boolean canVoid()
|
public boolean canVoid()
|
||||||
{
|
{
|
||||||
return this == DRAFT || this == CONFIRMED;
|
return this == DRAFT || this == CONFIRMED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 已确认或部分核销账单允许继续核销。 */
|
||||||
public boolean canWriteOff()
|
public boolean canWriteOff()
|
||||||
{
|
{
|
||||||
return this == CONFIRMED || this == PARTIAL_WRITTEN_OFF;
|
return this == CONFIRMED || this == PARTIAL_WRITTEN_OFF;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ruoyi.ar.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单导出任务已提交事件。
|
||||||
|
*
|
||||||
|
* <p>由事务提交后监听器处理,确保异步线程读取到已落库的导出任务。</p>
|
||||||
|
*/
|
||||||
|
public class ArBillExportTaskEvent
|
||||||
|
{
|
||||||
|
private final Long exportId;
|
||||||
|
|
||||||
|
public ArBillExportTaskEvent(Long exportId)
|
||||||
|
{
|
||||||
|
this.exportId = exportId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExportId()
|
||||||
|
{
|
||||||
|
return exportId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ruoyi.ar.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单异步生成任务已提交事件。
|
||||||
|
* 监听器在创建账单事务提交后消费,避免后台线程读取到未提交的任务或账单头。
|
||||||
|
*/
|
||||||
|
public class ArBillGenerateTaskEvent
|
||||||
|
{
|
||||||
|
/** 待投递的生成任务主键。 */
|
||||||
|
private final Long taskId;
|
||||||
|
|
||||||
|
public ArBillGenerateTaskEvent(Long taskId)
|
||||||
|
{
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTaskId()
|
||||||
|
{
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package com.ruoyi.ar.mapper;
|
|||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.ar.domain.ArBankAccount;
|
import com.ruoyi.ar.domain.ArBankAccount;
|
||||||
|
|
||||||
public interface ArBankAccountMapper
|
public interface ArBankAccountMapper {
|
||||||
{
|
|
||||||
ArBankAccount selectDefaultByCurrency(@Param("currency") String currency);
|
ArBankAccount selectDefaultByCurrency(@Param("currency") String currency);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package com.ruoyi.ar.mapper;
|
package com.ruoyi.ar.mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.ar.domain.ArBillDetailFee;
|
import com.ruoyi.ar.domain.ArBillDetailFee;
|
||||||
|
|
||||||
public interface ArBillDetailFeeMapper
|
public interface ArBillDetailFeeMapper {
|
||||||
{
|
|
||||||
List<ArBillDetailFee> selectByDetailId(Long detailId);
|
List<ArBillDetailFee> selectByDetailId(Long detailId);
|
||||||
|
|
||||||
|
List<ArBillDetailFee> selectByBillIdAndDetailId(@Param("arBillId") Long arBillId,
|
||||||
|
@Param("detailId") Long detailId);
|
||||||
|
|
||||||
List<ArBillDetailFee> selectByBillId(Long arBillId);
|
List<ArBillDetailFee> selectByBillId(Long arBillId);
|
||||||
|
|
||||||
|
List<ArBillDetailFee> selectByDetailIds(@Param("detailIds") List<Long> detailIds);
|
||||||
|
|
||||||
int insert(ArBillDetailFee fee);
|
int insert(ArBillDetailFee fee);
|
||||||
|
|
||||||
int insertBatch(List<ArBillDetailFee> fees);
|
int insertBatch(List<ArBillDetailFee> fees);
|
||||||
|
|
||||||
|
int deleteByBillId(Long arBillId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
package com.ruoyi.ar.mapper;
|
package com.ruoyi.ar.mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.ar.domain.ArBillDetail;
|
import com.ruoyi.ar.domain.ArBillDetail;
|
||||||
|
|
||||||
public interface ArBillDetailMapper
|
public interface ArBillDetailMapper {
|
||||||
{
|
|
||||||
List<ArBillDetail> selectByBillId(Long arBillId);
|
List<ArBillDetail> selectByBillId(Long arBillId);
|
||||||
|
|
||||||
/** 查询账单明细及费用明细,避免详情页逐层查询。 */
|
List<ArBillDetail> selectByBillIdAndSourceIds(@Param("arBillId") Long arBillId,
|
||||||
|
@Param("sourceBillIds") List<String> sourceBillIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账单明细及费用明细,避免详情页逐层查询。
|
||||||
|
*/
|
||||||
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
List<ArBillDetail> selectByBillIdWithFees(Long arBillId);
|
||||||
|
|
||||||
|
List<ArBillDetail> selectExportPage(@Param("arBillId") Long arBillId,
|
||||||
|
@Param("lastDetailNo") Integer lastDetailNo, @Param("pageSize") Integer pageSize);
|
||||||
|
|
||||||
int insert(ArBillDetail detail);
|
int insert(ArBillDetail detail);
|
||||||
|
|
||||||
int insertBatch(List<ArBillDetail> details);
|
int insertBatch(List<ArBillDetail> details);
|
||||||
|
|
||||||
|
int deleteByBillId(Long arBillId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package com.ruoyi.ar.mapper;
|
package com.ruoyi.ar.mapper;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.ar.domain.ArBillExport;
|
import com.ruoyi.ar.domain.ArBillExport;
|
||||||
|
|
||||||
public interface ArBillExportMapper
|
public interface ArBillExportMapper {
|
||||||
{
|
|
||||||
List<ArBillExport> selectByBillId(Long arBillId);
|
List<ArBillExport> selectByBillId(Long arBillId);
|
||||||
|
|
||||||
ArBillExport selectById(Long exportId);
|
ArBillExport selectById(Long exportId);
|
||||||
|
|
||||||
ArBillExport selectLatestSuccessByBillId(Long arBillId);
|
ArBillExport selectLatestSuccessByBillId(Long arBillId);
|
||||||
|
|
||||||
|
ArBillExport selectProcessingByBillId(Long arBillId);
|
||||||
|
|
||||||
int insert(ArBillExport export);
|
int insert(ArBillExport export);
|
||||||
|
|
||||||
|
int markSuccess(ArBillExport export);
|
||||||
|
|
||||||
|
int markFailed(@Param("exportId") Long exportId, @Param("remark") String remark);
|
||||||
|
|
||||||
|
int markExpiredProcessingAsFailed(@Param("expiredBefore") Date expiredBefore, @Param("remark") String remark);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.ar.mapper;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单异步生成任务 Mapper。
|
||||||
|
*/
|
||||||
|
public interface ArBillGenerateTaskMapper {
|
||||||
|
ArBillGenerateTask selectById(Long taskId);
|
||||||
|
|
||||||
|
ArBillGenerateTask selectByRequestToken(String requestToken);
|
||||||
|
|
||||||
|
List<ArBillGenerateTask> selectActiveByCreateBy(@Param("createBy") String createBy);
|
||||||
|
|
||||||
|
List<ArBillGenerateTask> selectWaiting(@Param("limit") int limit);
|
||||||
|
|
||||||
|
int insert(ArBillGenerateTask task);
|
||||||
|
|
||||||
|
int markRunning(@Param("taskId") Long taskId, @Param("startedAt") Date startedAt);
|
||||||
|
|
||||||
|
int updateTotalCount(@Param("taskId") Long taskId, @Param("totalCount") Integer totalCount);
|
||||||
|
|
||||||
|
int updateProgress(@Param("taskId") Long taskId, @Param("lastSourceBillId") String lastSourceBillId,
|
||||||
|
@Param("processedCount") Integer processedCount, @Param("successCount") Integer successCount,
|
||||||
|
@Param("totalAmount") java.math.BigDecimal totalAmount, @Param("heartbeatTime") Date heartbeatTime);
|
||||||
|
|
||||||
|
int markSuccess(@Param("taskId") Long taskId, @Param("finishedAt") Date finishedAt);
|
||||||
|
|
||||||
|
int markFailed(@Param("taskId") Long taskId, @Param("errorMessage") String errorMessage,
|
||||||
|
@Param("finishedAt") Date finishedAt);
|
||||||
|
|
||||||
|
int markCancelRequested(@Param("taskId") Long taskId);
|
||||||
|
|
||||||
|
int markCanceled(@Param("taskId") Long taskId, @Param("finishedAt") Date finishedAt);
|
||||||
|
|
||||||
|
int resetExpiredRunning(@Param("expiredBefore") Date expiredBefore);
|
||||||
|
|
||||||
|
List<ArBillGenerateTask> selectCancelRequestedBefore(@Param("expiredBefore") Date expiredBefore,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
}
|
||||||
@@ -3,35 +3,49 @@ package com.ruoyi.ar.mapper;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.ar.domain.ArBill;
|
import com.ruoyi.ar.domain.ArBill;
|
||||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||||
|
|
||||||
public interface ArBillMapper
|
public interface ArBillMapper {
|
||||||
{
|
|
||||||
List<ArBill> selectArBillList(ArBillQuery query);
|
List<ArBill> selectArBillList(ArBillQuery query);
|
||||||
|
|
||||||
ArBill selectArBillById(Long arBillId);
|
ArBill selectArBillById(Long arBillId);
|
||||||
|
|
||||||
ArBill selectArBillByBillNo(String billNo);
|
ArBill selectArBillByBillNo(String billNo);
|
||||||
|
|
||||||
|
ArBill selectGeneratingByCustomerPeriod(@Param("customerId") Long customerId,
|
||||||
|
@Param("currency") String currency, @Param("cutoffDate") Date cutoffDate);
|
||||||
|
|
||||||
Integer selectMaxBillVersion(@Param("customerId") Long customerId,
|
Integer selectMaxBillVersion(@Param("customerId") Long customerId,
|
||||||
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency);
|
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency);
|
||||||
|
|
||||||
ArBill selectArBillByIdForUpdate(Long arBillId);
|
ArBill selectArBillByIdForUpdate(Long arBillId);
|
||||||
|
|
||||||
/** 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择 */
|
/**
|
||||||
|
* 查询客户可核销账单(已确认/部分核销),按账单日期升序,供余额核销选择
|
||||||
|
*/
|
||||||
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId);
|
List<ArBill> selectUnsettledByCustomer(@Param("customerId") Long customerId);
|
||||||
|
|
||||||
int insertArBill(ArBill bill);
|
int insertArBill(ArBill bill);
|
||||||
|
|
||||||
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
||||||
@Param("remark") String remark, @Param("operator") String operator,
|
@Param("remark") String remark, @Param("operator") String operator,
|
||||||
@Param("fromStatus") String fromStatus);
|
@Param("fromStatus") String fromStatus);
|
||||||
|
|
||||||
int updateArBillAmounts(@Param("arBillId") Long arBillId,
|
int updateArBillAmounts(@Param("arBillId") Long arBillId,
|
||||||
@Param("writtenOffAmount") BigDecimal writtenOffAmount,
|
@Param("writtenOffAmount") BigDecimal writtenOffAmount,
|
||||||
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
||||||
@Param("status") String status, @Param("operator") String operator,
|
@Param("status") String status, @Param("operator") String operator,
|
||||||
@Param("fromStatus") String fromStatus);
|
@Param("fromStatus") String fromStatus);
|
||||||
|
|
||||||
|
int updateArBillProgress(@Param("arBillId") Long arBillId,
|
||||||
|
@Param("orderCount") Integer orderCount, @Param("totalAmount") BigDecimal totalAmount,
|
||||||
|
@Param("unwrittenAmount") BigDecimal unwrittenAmount,
|
||||||
|
@Param("billPeriodStart") Date billPeriodStart, @Param("operator") String operator);
|
||||||
|
|
||||||
|
int updateArBillGenerationStatus(@Param("arBillId") Long arBillId,
|
||||||
|
@Param("generationStatus") String generationStatus, @Param("operator") String operator,
|
||||||
|
@Param("remark") String remark);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ruoyi.ar.mapper;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.ruoyi.ar.domain.ArExportTemplate;
|
||||||
|
|
||||||
|
public interface ArExportTemplateMapper {
|
||||||
|
/**
|
||||||
|
* 查询指定业务类型当前生效的默认模板。
|
||||||
|
*/
|
||||||
|
ArExportTemplate selectDefaultEnabled(@Param("businessType") String businessType,
|
||||||
|
@Param("effectiveDate") Date effectiveDate);
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.ruoyi.ar.service;
|
package com.ruoyi.ar.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.ruoyi.ar.domain.ArBillLog;
|
import com.ruoyi.ar.domain.ArBillLog;
|
||||||
import com.ruoyi.ar.enums.ArLogObjectType;
|
import com.ruoyi.ar.enums.ArLogObjectType;
|
||||||
import com.ruoyi.ar.enums.ArLogOperationType;
|
import com.ruoyi.ar.enums.ArLogOperationType;
|
||||||
|
|
||||||
public interface IArBillLogService
|
public interface IArBillLogService {
|
||||||
{
|
|
||||||
List<ArBillLog> selectLogList(ArBillLog query);
|
List<ArBillLog> selectLogList(ArBillLog query);
|
||||||
|
|
||||||
void record(ArBillLog log);
|
void record(ArBillLog log);
|
||||||
|
|
||||||
void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
||||||
String fromStatus, String message, String operator, String requestId);
|
String fromStatus, String message, String operator, String requestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package com.ruoyi.ar.service;
|
package com.ruoyi.ar.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.ruoyi.ar.domain.ArBill;
|
import com.ruoyi.ar.domain.ArBill;
|
||||||
import com.ruoyi.ar.domain.ArBankAccount;
|
import com.ruoyi.ar.domain.ArBankAccount;
|
||||||
|
import com.ruoyi.ar.domain.ArBillDetail;
|
||||||
|
import com.ruoyi.ar.domain.ArBillDetailFee;
|
||||||
import com.ruoyi.ar.domain.ArBillExport;
|
import com.ruoyi.ar.domain.ArBillExport;
|
||||||
|
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||||
import com.ruoyi.ar.domain.ArBillLog;
|
import com.ruoyi.ar.domain.ArBillLog;
|
||||||
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
import com.ruoyi.ar.domain.vo.ArBillGenerateRequest;
|
||||||
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
import com.ruoyi.ar.domain.vo.ArBillGenerateQuery;
|
||||||
@@ -11,17 +16,48 @@ import com.ruoyi.ar.domain.vo.ArBillActionRequest;
|
|||||||
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
import com.ruoyi.ar.domain.vo.ArBillQuery;
|
||||||
import com.ruoyi.price.domain.PrcOrderBill;
|
import com.ruoyi.price.domain.PrcOrderBill;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单业务接口。
|
||||||
|
* 查询接口面向列表/详情页面,生成和导出接口面向异步任务执行器;
|
||||||
|
* Controller 不直接操作 Mapper,统一由这里维护业务状态和事务边界。
|
||||||
|
*/
|
||||||
public interface IArBillService
|
public interface IArBillService
|
||||||
{
|
{
|
||||||
List<ArBill> selectArBillList(ArBillQuery query);
|
List<ArBill> selectArBillList(ArBillQuery query);
|
||||||
|
|
||||||
ArBill selectArBillById(Long arBillId);
|
ArBill selectArBillById(Long arBillId);
|
||||||
|
|
||||||
|
List<ArBillDetail> selectArBillDetails(Long arBillId);
|
||||||
|
|
||||||
|
List<ArBillDetailFee> selectArBillDetailFees(Long arBillId, Long detailId);
|
||||||
|
|
||||||
ArBankAccount getDefaultBankAccount(String currency);
|
ArBankAccount getDefaultBankAccount(String currency);
|
||||||
|
|
||||||
List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
|
List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
|
||||||
|
|
||||||
ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId);
|
BigDecimal sumPreviewBill(ArBillGenerateQuery query);
|
||||||
|
|
||||||
|
ArBillGenerateTask generateBill(ArBillGenerateRequest request, String operator, String requestId);
|
||||||
|
|
||||||
|
ArBillGenerateTask selectGenerateTask(Long taskId);
|
||||||
|
|
||||||
|
List<ArBillGenerateTask> selectActiveGenerateTasks(String operator);
|
||||||
|
|
||||||
|
ArBillGenerateTask cancelGenerateTask(Long taskId, String operator, String requestId);
|
||||||
|
|
||||||
|
ArBillGenerateTask retryGenerateTask(Long taskId, String operator, String requestId);
|
||||||
|
|
||||||
|
void processGenerateTask(Long taskId);
|
||||||
|
|
||||||
|
boolean claimGenerateTask(Long taskId);
|
||||||
|
|
||||||
|
void failGenerateTask(Long taskId, String remark);
|
||||||
|
|
||||||
|
int recoverExpiredGenerateTasks(Date expiredBefore);
|
||||||
|
|
||||||
|
int recoverCancelRequestedGenerateTasks(Date expiredBefore);
|
||||||
|
|
||||||
|
List<ArBillGenerateTask> selectWaitingGenerateTasks(int limit);
|
||||||
|
|
||||||
ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
|
ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId);
|
||||||
|
|
||||||
@@ -29,6 +65,14 @@ public interface IArBillService
|
|||||||
|
|
||||||
ArBillExport exportBill(Long arBillId, String operator, String requestId);
|
ArBillExport exportBill(Long arBillId, String operator, String requestId);
|
||||||
|
|
||||||
|
ArBillExport selectExport(Long exportId);
|
||||||
|
|
||||||
|
void processExportTask(Long exportId);
|
||||||
|
|
||||||
|
void failExportTask(Long exportId, String remark);
|
||||||
|
|
||||||
|
int failExpiredExportTasks(Date expiredBefore);
|
||||||
|
|
||||||
List<ArBillExport> selectExports(Long arBillId);
|
List<ArBillExport> selectExports(Long arBillId);
|
||||||
|
|
||||||
void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception;
|
void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception;
|
||||||
|
|||||||
+51
-59
@@ -7,6 +7,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -39,8 +40,7 @@ import com.ruoyi.customer.customermoney.domain.CustomerBalanceFlow;
|
|||||||
* @date 2026-09-16
|
* @date 2026-09-16
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService {
|
||||||
{
|
|
||||||
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
private static final DateTimeFormatter NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||||
private static final int MONEY_SCALE = 2;
|
private static final int MONEY_SCALE = 2;
|
||||||
|
|
||||||
@@ -56,55 +56,55 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ICustomerAccountApi customerAccountApi;
|
private ICustomerAccountApi customerAccountApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按客户查询允许余额核销的未结清账单。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<ArBill> selectUnsettledBills(Long customerId)
|
public List<ArBill> selectUnsettledBills(Long customerId) {
|
||||||
{
|
if (customerId == null) {
|
||||||
if (customerId == null)
|
|
||||||
{
|
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return billMapper.selectUnsettledByCustomer(customerId);
|
return billMapper.selectUnsettledByCustomer(customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账单余额核销记录列表。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query)
|
public List<ArBalanceWriteoff> selectWriteoffList(ArBalanceWriteoff query) {
|
||||||
{
|
|
||||||
return writeoffMapper.selectWriteoffList(query);
|
return writeoffMapper.selectWriteoffList(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行余额核销主流程:锁账单、登记核销单、扣减客户余额、更新账单金额和状态。
|
||||||
|
* 任一步失败都会回滚当前事务,避免核销单、资金流水和账单状态不一致。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd)
|
public ArBalanceWriteoff writeoff(ArBalanceWriteoff cmd) {
|
||||||
{
|
if (cmd == null || cmd.getArBillId() == null) {
|
||||||
if (cmd == null || cmd.getArBillId() == null)
|
|
||||||
{
|
|
||||||
throw new ServiceException("请选择要核销的账单");
|
throw new ServiceException("请选择要核销的账单");
|
||||||
}
|
}
|
||||||
BigDecimal amount = cmd.getWriteoffAmount();
|
BigDecimal amount = cmd.getWriteoffAmount();
|
||||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0)
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
{
|
|
||||||
throw new ServiceException("核销金额必须大于0");
|
throw new ServiceException("核销金额必须大于0");
|
||||||
}
|
}
|
||||||
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
amount = amount.setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
|
// 1. 行锁锁定账单(加锁顺序:账单 -> 账户,与反核销保持一致)
|
||||||
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
|
ArBill bill = billMapper.selectArBillByIdForUpdate(cmd.getArBillId());
|
||||||
if (bill == null)
|
if (bill == null) {
|
||||||
{
|
|
||||||
throw new ServiceException("应收账单不存在");
|
throw new ServiceException("应收账单不存在");
|
||||||
}
|
}
|
||||||
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
ArBillStatus status = ArBillStatus.of(bill.getStatus());
|
||||||
if (status == null || !status.canWriteOff())
|
if (status == null || !status.canWriteOff()) {
|
||||||
{
|
|
||||||
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
throw new ServiceException("当前账单状态不允许核销:" + (status == null ? bill.getStatus() : status.getLabel()));
|
||||||
}
|
}
|
||||||
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId()))
|
if (cmd.getCustomerId() != null && !cmd.getCustomerId().equals(bill.getCustomerId())) {
|
||||||
{
|
|
||||||
throw new ServiceException("账单与所选客户不匹配");
|
throw new ServiceException("账单与所选客户不匹配");
|
||||||
}
|
}
|
||||||
BigDecimal unwritten = nvl(bill.getUnwrittenAmount());
|
BigDecimal unwritten = nvl(bill.getUnwrittenAmount());
|
||||||
if (amount.compareTo(unwritten) > 0)
|
if (amount.compareTo(unwritten) > 0) {
|
||||||
{
|
|
||||||
throw new ServiceException("核销金额超过账单未核销金额:" + unwritten.toPlainString());
|
throw new ServiceException("核销金额超过账单未核销金额:" + unwritten.toPlainString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
|
|
||||||
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
// 3. 扣减账户余额并释放等额已用额度,写 BILL_PAY 扣款流水(余额不足在此处拦截)
|
||||||
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
String flowRemark = "余额核销账单:" + bill.getBillNo()
|
||||||
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
+ (StringUtils.hasText(writeoff.getRemark()) ? "(" + writeoff.getRemark() + ")" : "");
|
||||||
BalanceChangeCommand payCmd = new BalanceChangeCommand();
|
BalanceChangeCommand payCmd = new BalanceChangeCommand();
|
||||||
payCmd.setCustomerId(bill.getCustomerId());
|
payCmd.setCustomerId(bill.getCustomerId());
|
||||||
payCmd.setCurrency(currency);
|
payCmd.setCurrency(currency);
|
||||||
@@ -147,40 +147,38 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
BigDecimal newUnwritten = unwritten.subtract(amount);
|
BigDecimal newUnwritten = unwritten.subtract(amount);
|
||||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||||
nextStatus, operator, fromStatus) != 1)
|
nextStatus, operator, fromStatus) != 1) {
|
||||||
{
|
|
||||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
recordLog(bill.getArBillId(), writeoff.getWriteoffId(), ArLogOperationType.BALANCE_WRITEOFF,
|
||||||
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
"账户余额核销账单 " + amount.toPlainString() + " " + currency,
|
||||||
fromStatus, nextStatus, operator, requestId);
|
fromStatus, nextStatus, operator, requestId);
|
||||||
writeoff.setAccountId(flow.getAccountId());
|
writeoff.setAccountId(flow.getAccountId());
|
||||||
writeoff.setBalanceFlowId(flow.getId());
|
writeoff.setBalanceFlowId(flow.getId());
|
||||||
return writeoff;
|
return writeoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行反核销主流程:锁定核销单和账单、回补客户余额、恢复账单未核销金额,
|
||||||
|
* 最后将原核销记录标记为已取消。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void cancelWriteoff(Long writeoffId, String remark)
|
public void cancelWriteoff(Long writeoffId, String remark) {
|
||||||
{
|
if (writeoffId == null) {
|
||||||
if (writeoffId == null)
|
|
||||||
{
|
|
||||||
throw new ServiceException("核销记录不存在");
|
throw new ServiceException("核销记录不存在");
|
||||||
}
|
}
|
||||||
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
// 1. 锁核销单 -> 锁账单(与核销相同的加锁顺序)
|
||||||
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
ArBalanceWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId);
|
||||||
if (writeoff == null)
|
if (writeoff == null) {
|
||||||
{
|
|
||||||
throw new ServiceException("核销记录不存在");
|
throw new ServiceException("核销记录不存在");
|
||||||
}
|
}
|
||||||
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus()))
|
if (!ArBalanceWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) {
|
||||||
{
|
|
||||||
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
throw new ServiceException("只有有效状态的核销记录可以反核销");
|
||||||
}
|
}
|
||||||
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId());
|
||||||
if (bill == null)
|
if (bill == null) {
|
||||||
{
|
|
||||||
throw new ServiceException("核销关联的账单不存在");
|
throw new ServiceException("核销关联的账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,44 +202,40 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
|
|
||||||
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
// 3. 账单核销金额回退,状态按剩余已核销额重算(归零则回到已确认)
|
||||||
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(nvl(writeoff.getWriteoffAmount()));
|
||||||
if (written.compareTo(BigDecimal.ZERO) < 0)
|
if (written.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
{
|
|
||||||
written = BigDecimal.ZERO;
|
written = BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
BigDecimal newUnwritten = nvl(bill.getTotalAmount()).subtract(written);
|
BigDecimal newUnwritten = nvl(bill.getTotalAmount()).subtract(written);
|
||||||
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
String nextStatus = billStatus(written, nvl(bill.getTotalAmount()), fromStatus);
|
||||||
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
if (billMapper.updateArBillAmounts(bill.getArBillId(), written, newUnwritten,
|
||||||
nextStatus, operator, fromStatus) != 1)
|
nextStatus, operator, fromStatus) != 1) {
|
||||||
{
|
|
||||||
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
throw new ServiceException("账单状态已发生变化,请刷新后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 核销单置为CANCELLED并登记回补流水
|
// 4. 核销单置为CANCELLED并登记回补流水
|
||||||
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark,
|
if (writeoffMapper.updateCancelled(writeoffId, reverseFlow.getId(), operator, cancelRemark,
|
||||||
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1)
|
ArBalanceWriteoffStatus.CANCELLED.getCode(), ArBalanceWriteoffStatus.EFFECTIVE.getCode()) != 1) {
|
||||||
{
|
|
||||||
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
throw new ServiceException("核销记录状态已变化,请刷新后重试");
|
||||||
}
|
}
|
||||||
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
recordLog(bill.getArBillId(), writeoffId, ArLogOperationType.CANCEL_BALANCE_WRITEOFF,
|
||||||
"反核销回补账单:" + cancelRemark,
|
"反核销回补账单:" + cancelRemark,
|
||||||
fromStatus, nextStatus, operator, requestId);
|
fromStatus, nextStatus, operator, requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 账单状态重算:已核销归零(非草稿)-> 已确认;足额 -> 已核销;否则部分核销 */
|
/**
|
||||||
private String billStatus(BigDecimal written, BigDecimal total, String current)
|
* 账单状态重算:归零(非草稿)为已确认,足额为已核销,否则为部分核销。
|
||||||
{
|
*/
|
||||||
if (written.compareTo(BigDecimal.ZERO) == 0)
|
private String billStatus(BigDecimal written, BigDecimal total, String current) {
|
||||||
{
|
if (written.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
return ArBillStatus.DRAFT.getCode().equals(current)
|
return ArBillStatus.DRAFT.getCode().equals(current)
|
||||||
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
|
? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode();
|
||||||
}
|
}
|
||||||
return written.compareTo(total) >= 0
|
return written.compareTo(total) >= 0
|
||||||
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
|
? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordLog(Long billId, Long writeoffId, ArLogOperationType operationType, String desc,
|
private void recordLog(Long billId, Long writeoffId, ArLogOperationType operationType, String desc,
|
||||||
String fromStatus, String toStatus, String operator, String requestId)
|
String fromStatus, String toStatus, String operator, String requestId) {
|
||||||
{
|
|
||||||
ArBillLog log = new ArBillLog();
|
ArBillLog log = new ArBillLog();
|
||||||
log.setArBillId(billId);
|
log.setArBillId(billId);
|
||||||
log.setObjectType(ArLogObjectType.BALANCE_WRITEOFF.getCode());
|
log.setObjectType(ArLogObjectType.BALANCE_WRITEOFF.getCode());
|
||||||
@@ -258,14 +252,12 @@ public class ArBalanceWriteoffServiceImpl implements IArBalanceWriteoffService
|
|||||||
logService.record(log);
|
logService.record(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateNo()
|
private String generateNo() {
|
||||||
{
|
|
||||||
return "BWO" + LocalDateTime.now().format(NO_FORMATTER)
|
return "BWO" + LocalDateTime.now().format(NO_FORMATTER)
|
||||||
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
+ ThreadLocalRandom.current().nextInt(100, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal nvl(BigDecimal value)
|
private BigDecimal nvl(BigDecimal value) {
|
||||||
{
|
|
||||||
return value == null ? BigDecimal.ZERO : value;
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-9
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.ar.service.impl;
|
package com.ruoyi.ar.service.impl;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
@@ -15,29 +16,27 @@ import com.ruoyi.ar.service.IArBillLogService;
|
|||||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ArBillLogServiceImpl implements IArBillLogService
|
public class ArBillLogServiceImpl implements IArBillLogService {
|
||||||
{
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ArBillLogMapper logMapper;
|
private ArBillLogMapper logMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ArBillLog> selectLogList(ArBillLog query)
|
public List<ArBillLog> selectLogList(ArBillLog query) {
|
||||||
{
|
|
||||||
return logMapper.selectList(query);
|
return logMapper.selectList(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void record(ArBillLog log)
|
public void record(ArBillLog log) {
|
||||||
{
|
|
||||||
logMapper.insert(log);
|
logMapper.insert(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 失败日志必须独立提交,不能随业务异常一起回滚。 */
|
/**
|
||||||
|
* 失败日志必须独立提交,不能随业务异常一起回滚。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
public void recordFailure(Long arBillId, ArLogObjectType objectType, String objectId, ArLogOperationType operationType,
|
||||||
String fromStatus, String message, String operator, String requestId)
|
String fromStatus, String message, String operator, String requestId) {
|
||||||
{
|
|
||||||
ArBillLog log = new ArBillLog();
|
ArBillLog log = new ArBillLog();
|
||||||
log.setArBillId(arBillId);
|
log.setArBillId(arBillId);
|
||||||
log.setObjectType(objectType.getCode());
|
log.setObjectType(objectType.getCode());
|
||||||
|
|||||||
+1234
-493
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
|||||||
|
package com.ruoyi.ar.task;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.core.task.TaskRejectedException;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
import com.ruoyi.ar.event.ArBillExportTaskEvent;
|
||||||
|
import com.ruoyi.ar.service.IArBillService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单导出异步任务执行器。
|
||||||
|
* 导出文件写入服务器临时目录,Web 请求只负责提交任务,完成后再提供下载。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ArBillExportTaskRunner {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ArBillExportTaskRunner.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出超过 60 分钟仍未完成时,视为异常任务。
|
||||||
|
*/
|
||||||
|
private static final long PROCESSING_TIMEOUT_MILLIS = 60L * 60L * 1000L;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IArBillService billService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("arBillExportTaskExecutor")
|
||||||
|
private ThreadPoolTaskExecutor exportTaskExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅在提交导出任务的事务成功提交后异步执行,避免读取到未提交数据。
|
||||||
|
*/
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
/** 事务提交后投递到导出专用线程池,避免占用请求线程。 */
|
||||||
|
public void run(ArBillExportTaskEvent event) {
|
||||||
|
try {
|
||||||
|
exportTaskExecutor.execute(() -> billService.processExportTask(event.getExportId()));
|
||||||
|
} catch (TaskRejectedException e) {
|
||||||
|
billService.failExportTask(event.getExportId(), "导出任务较多,请稍后重新发起导出");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定期关闭因应用中断等原因长期未完成的导出任务。
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0/5 * * * ?")
|
||||||
|
public void failExpiredTasks() {
|
||||||
|
Date expiredBefore = new Date(System.currentTimeMillis() - PROCESSING_TIMEOUT_MILLIS);
|
||||||
|
int count = billService.failExpiredExportTasks(expiredBefore);
|
||||||
|
if (count > 0) {
|
||||||
|
log.warn("已关闭 {} 个超时的应收账单导出任务", count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.ruoyi.ar.task;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.core.task.TaskRejectedException;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
import com.ruoyi.ar.domain.ArBillGenerateTask;
|
||||||
|
import com.ruoyi.ar.event.ArBillGenerateTaskEvent;
|
||||||
|
import com.ruoyi.ar.service.IArBillService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应收账单异步生成任务调度器。
|
||||||
|
*
|
||||||
|
* <p>事务提交后立即尝试投递任务;定时补偿负责恢复节点异常、清理超时取消任务,
|
||||||
|
* 并重新投递仍处于 WAITING 状态的任务。</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ArBillGenerateTaskRunner {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ArBillGenerateTaskRunner.class);
|
||||||
|
/**
|
||||||
|
* 生成任务超过 15 分钟没有心跳,认为执行节点异常并恢复为待处理。
|
||||||
|
*/
|
||||||
|
private static final long PROCESSING_TIMEOUT_MILLIS = 15L * 60L * 1000L;
|
||||||
|
/**
|
||||||
|
* 取消请求超过 5 分钟没有完成,交由补偿逻辑清理账单和明细。
|
||||||
|
*/
|
||||||
|
private static final long CANCELLATION_TIMEOUT_MILLIS = 5L * 60L * 1000L;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IArBillService billService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("arBillGenerateTaskExecutor")
|
||||||
|
private ThreadPoolTaskExecutor generateTaskExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单生成事务提交后触发,避免事务回滚时提前执行异步任务。
|
||||||
|
*/
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void run(ArBillGenerateTaskEvent event) {
|
||||||
|
submit(event.getTaskId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每 30 秒执行一次任务恢复、取消清理和待处理任务投递。
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0/30 * * * * ?")
|
||||||
|
public void recoverAndSubmitTasks() {
|
||||||
|
try {
|
||||||
|
Date expiredBefore = new Date(System.currentTimeMillis() - PROCESSING_TIMEOUT_MILLIS);
|
||||||
|
int recovered = billService.recoverExpiredGenerateTasks(expiredBefore);
|
||||||
|
if (recovered > 0) {
|
||||||
|
log.warn("已恢复 {} 个超时的应收账单生成任务", recovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
Date cancellationExpiredBefore = new Date(System.currentTimeMillis() - CANCELLATION_TIMEOUT_MILLIS);
|
||||||
|
int canceled = billService.recoverCancelRequestedGenerateTasks(cancellationExpiredBefore);
|
||||||
|
if (canceled > 0) {
|
||||||
|
log.warn("已自动清理 {} 个超时的应收账单取消任务", canceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ArBillGenerateTask task : billService.selectWaitingGenerateTasks(5)) {
|
||||||
|
submit(task.getTaskId());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("应收账单生成任务调度失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 先用数据库条件更新抢占任务,再提交到有界线程池,避免重复执行。
|
||||||
|
*/
|
||||||
|
private void submit(Long taskId) {
|
||||||
|
try {
|
||||||
|
if (!billService.claimGenerateTask(taskId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generateTaskExecutor.execute(() -> billService.processGenerateTask(taskId));
|
||||||
|
} catch (TaskRejectedException e) {
|
||||||
|
log.error("应收账单生成任务提交线程池失败,taskId={}", taskId, e);
|
||||||
|
try {
|
||||||
|
billService.failGenerateTask(taskId, "生成任务较多,请稍后重新发起");
|
||||||
|
} catch (Exception cleanupException) {
|
||||||
|
log.error("线程池拒绝后,清理应收账单生成任务失败,taskId={}", taskId, cleanupException);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("应收账单生成任务提交失败,taskId={}", taskId, e);
|
||||||
|
try {
|
||||||
|
billService.failGenerateTask(taskId, "生成任务提交失败,请稍后重新发起");
|
||||||
|
} catch (Exception cleanupException) {
|
||||||
|
log.error("任务提交异常后,清理应收账单生成任务失败,taskId={}", taskId, cleanupException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,12 +20,36 @@ public interface PrcOrderBillMapper
|
|||||||
|
|
||||||
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
|
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
|
||||||
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
|
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
|
||||||
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
|
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||||
@Param("currency") String currency, @Param("arBillStatus") String arBillStatus);
|
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
|
||||||
|
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||||
|
@Param("arBillStatus") String arBillStatus, @Param("excludedBillIds") List<String> excludedBillIds);
|
||||||
|
|
||||||
|
/** 统计应收账单预览结果金额,供全选模式展示。 */
|
||||||
|
BigDecimal sumPrcOrderBillIncomeByCustomerPeriod(@Param("customerId") Long customerId,
|
||||||
|
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||||
|
@Param("excludeOrderOrTrackingNo") String excludeOrderOrTrackingNo,
|
||||||
|
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||||
|
@Param("arBillStatus") String arBillStatus);
|
||||||
|
|
||||||
|
/** 异步生成账单按 bill_id 游标读取一批来源订单。 */
|
||||||
|
List<PrcOrderBill> selectPrcOrderBillByCustomerPeriodCursor(@Param("customerId") Long customerId,
|
||||||
|
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||||
|
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||||
|
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
|
||||||
|
@Param("lastSourceBillId") String lastSourceBillId, @Param("billIds") List<String> billIds,
|
||||||
|
@Param("excludedBillIds") List<String> excludedBillIds, @Param("limit") int limit);
|
||||||
|
|
||||||
|
int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
|
||||||
|
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||||
|
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
|
||||||
|
@Param("arBillStatus") String arBillStatus, @Param("snapshotCreateTime") Date snapshotCreateTime,
|
||||||
|
@Param("billIds") List<String> billIds, @Param("excludedBillIds") List<String> excludedBillIds);
|
||||||
|
|
||||||
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
|
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
|
||||||
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
|
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
|
||||||
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
|
@Param("customerCode") String customerCode, @Param("orderOrTrackingNo") String orderOrTrackingNo,
|
||||||
|
@Param("cutoffDate") Date cutoffDate,
|
||||||
@Param("currency") String currency, @Param("billIds") List<String> billIds,
|
@Param("currency") String currency, @Param("billIds") List<String> billIds,
|
||||||
@Param("arBillStatus") String arBillStatus);
|
@Param("arBillStatus") String arBillStatus);
|
||||||
|
|
||||||
@@ -37,6 +61,9 @@ public interface PrcOrderBillMapper
|
|||||||
public int releaseArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
|
public int releaseArBillOrders(@Param("billIds") List<String> billIds, @Param("operator") String operator,
|
||||||
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||||
|
|
||||||
|
int releaseArBillOrdersByArBillId(@Param("arBillId") Long arBillId, @Param("operator") String operator,
|
||||||
|
@Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus);
|
||||||
|
|
||||||
public int insertPrcOrderBill(PrcOrderBill bill);
|
public int insertPrcOrderBill(PrcOrderBill bill);
|
||||||
|
|
||||||
public int updatePrcOrderBill(PrcOrderBill bill);
|
public int updatePrcOrderBill(PrcOrderBill bill);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBankAccountMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBankAccountMapper">
|
||||||
<resultMap id="ArBankAccountResult" type="ArBankAccount">
|
<resultMap id="ArBankAccountResult" type="ArBankAccount">
|
||||||
<id property="bankAccountId" column="bank_account_id"/>
|
<id property="bankAccountId" column="bank_account_id"/>
|
||||||
@@ -19,12 +19,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectDefaultByCurrency" resultMap="ArBankAccountResult">
|
<select id="selectDefaultByCurrency" resultMap="ArBankAccountResult">
|
||||||
select bank_account_id, account_name, account_no, bank_name, currency,
|
select bank_account_id,
|
||||||
is_default, status, remark, create_by, create_time, update_by, update_time
|
account_name,
|
||||||
|
account_no,
|
||||||
|
bank_name,
|
||||||
|
currency,
|
||||||
|
is_default,
|
||||||
|
status,
|
||||||
|
remark,
|
||||||
|
create_by,
|
||||||
|
create_time,
|
||||||
|
update_by,
|
||||||
|
update_time
|
||||||
from ar_bank_account
|
from ar_bank_account
|
||||||
where currency = #{currency}
|
where currency = #{currency}
|
||||||
and status = '0'
|
and status = '0'
|
||||||
order by is_default desc, bank_account_id
|
order by is_default desc, bank_account_id limit 1
|
||||||
limit 1
|
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBillDetailFeeMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillDetailFeeMapper">
|
||||||
|
<!-- 账单明细费用项读写;批量查询仅接收当前明细批次的 ID。 -->
|
||||||
<resultMap id="ArBillDetailFeeResult" type="ArBillDetailFee">
|
<resultMap id="ArBillDetailFeeResult" type="ArBillDetailFee">
|
||||||
<id property="detailFeeId" column="detail_fee_id"/>
|
<id property="detailFeeId" column="detail_fee_id"/>
|
||||||
<result property="detailId" column="detail_id"/>
|
<result property="detailId" column="detail_id"/>
|
||||||
@@ -24,39 +25,116 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectByDetailId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
<select id="selectByDetailId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
||||||
select detail_fee_id, detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
select detail_fee_id,
|
||||||
unit, unit_price, amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
detail_id,
|
||||||
from ar_bill_detail_fee where detail_id = #{detailId} order by fee_no, detail_fee_id
|
prc_income_bill_fee_id,
|
||||||
|
fee_no,
|
||||||
|
fee_type,
|
||||||
|
fee_name,
|
||||||
|
sc_id,
|
||||||
|
quantity,
|
||||||
|
unit,
|
||||||
|
unit_price,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
calculation_formula,
|
||||||
|
round_mode,
|
||||||
|
remark,
|
||||||
|
create_by,
|
||||||
|
create_time
|
||||||
|
from ar_bill_detail_fee
|
||||||
|
where detail_id = #{detailId}
|
||||||
|
order by fee_no, detail_fee_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByBillIdAndDetailId" resultMap="ArBillDetailFeeResult">
|
||||||
|
select f.detail_fee_id,
|
||||||
|
f.detail_id,
|
||||||
|
f.prc_income_bill_fee_id,
|
||||||
|
f.fee_no,
|
||||||
|
f.fee_type,
|
||||||
|
f.fee_name,
|
||||||
|
f.sc_id,
|
||||||
|
f.quantity,
|
||||||
|
f.unit,
|
||||||
|
f.unit_price,
|
||||||
|
f.amount,
|
||||||
|
f.currency,
|
||||||
|
f.calculation_formula,
|
||||||
|
f.round_mode,
|
||||||
|
f.remark,
|
||||||
|
f.create_by,
|
||||||
|
f.create_time
|
||||||
|
from ar_bill_detail_fee f
|
||||||
|
inner join ar_bill_detail d on d.detail_id = f.detail_id
|
||||||
|
where d.ar_bill_id = #{arBillId}
|
||||||
|
and f.detail_id = #{detailId}
|
||||||
|
order by f.fee_no, f.detail_fee_id
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
<select id="selectByBillId" parameterType="long" resultMap="ArBillDetailFeeResult">
|
||||||
select detail_fee_id, detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
select detail_fee_id,
|
||||||
unit, unit_price, amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
detail_id,
|
||||||
|
prc_income_bill_fee_id,
|
||||||
|
fee_no,
|
||||||
|
fee_type,
|
||||||
|
fee_name,
|
||||||
|
sc_id,
|
||||||
|
quantity,
|
||||||
|
unit,
|
||||||
|
unit_price,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
calculation_formula,
|
||||||
|
round_mode,
|
||||||
|
remark,
|
||||||
|
create_by,
|
||||||
|
create_time
|
||||||
from ar_bill_detail_fee
|
from ar_bill_detail_fee
|
||||||
where detail_id in (select detail_id from ar_bill_detail where ar_bill_id = #{arBillId})
|
where detail_id in (select detail_id from ar_bill_detail where ar_bill_id = #{arBillId})
|
||||||
order by detail_id, fee_no, detail_fee_id
|
order by detail_id, fee_no, detail_fee_id
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByDetailIds" resultMap="ArBillDetailFeeResult">
|
||||||
|
select detail_fee_id, detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||||
|
unit, unit_price, amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||||
|
from ar_bill_detail_fee
|
||||||
|
where detail_id in
|
||||||
|
<foreach collection="detailIds" item="detailId" open="(" separator="," close=")">
|
||||||
|
#{detailId}
|
||||||
|
</foreach>
|
||||||
|
order by detail_id, fee_no, detail_fee_id
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="ArBillDetailFee" useGeneratedKeys="true" keyProperty="detailFeeId">
|
<insert id="insert" parameterType="ArBillDetailFee" useGeneratedKeys="true" keyProperty="detailFeeId">
|
||||||
insert into ar_bill_detail_fee (
|
insert into ar_bill_detail_fee (detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity,
|
||||||
detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
unit, unit_price,
|
||||||
amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
amount, currency, calculation_formula, round_mode, remark, create_by,
|
||||||
) values (
|
create_time)
|
||||||
#{detailId}, #{prcIncomeBillFeeId}, #{feeNo}, #{feeType}, #{feeName}, #{scId}, #{quantity}, #{unit},
|
values (#{detailId}, #{prcIncomeBillFeeId}, #{feeNo}, #{feeType}, #{feeName}, #{scId}, #{quantity}, #{unit},
|
||||||
#{unitPrice}, #{amount}, #{currency}, #{calculationFormula}, #{roundMode}, #{remark},
|
#{unitPrice}, #{amount}, #{currency}, #{calculationFormula}, #{roundMode}, #{remark},
|
||||||
#{createBy}, sysdate()
|
#{createBy}, sysdate())
|
||||||
)
|
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailFeeId">
|
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailFeeId">
|
||||||
insert into ar_bill_detail_fee (
|
insert into ar_bill_detail_fee (
|
||||||
detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
detail_id, prc_income_bill_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price,
|
||||||
amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
amount, currency, calculation_formula, round_mode, remark, create_by, create_time
|
||||||
) values
|
) values
|
||||||
<foreach collection="list" item="item" separator=",">
|
<foreach collection="list" item="item" separator=",">
|
||||||
(#{item.detailId}, #{item.prcIncomeBillFeeId}, #{item.feeNo}, #{item.feeType}, #{item.feeName}, #{item.scId},
|
(#{item.detailId}, #{item.prcIncomeBillFeeId}, #{item.feeNo}, #{item.feeType}, #{item.feeName},
|
||||||
#{item.quantity}, #{item.unit}, #{item.unitPrice}, #{item.amount}, #{item.currency},
|
#{item.scId},
|
||||||
#{item.calculationFormula}, #{item.roundMode}, #{item.remark}, #{item.createBy}, sysdate())
|
#{item.quantity}, #{item.unit}, #{item.unitPrice}, #{item.amount}, #{item.currency},
|
||||||
|
#{item.calculationFormula}, #{item.roundMode}, #{item.remark}, #{item.createBy}, sysdate())
|
||||||
</foreach>
|
</foreach>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<delete id="deleteByBillId" parameterType="long">
|
||||||
|
delete
|
||||||
|
f
|
||||||
|
from ar_bill_detail_fee f
|
||||||
|
inner join ar_bill_detail d on d.detail_id = f.detail_id
|
||||||
|
where d.ar_bill_id =
|
||||||
|
#{arBillId}
|
||||||
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBillDetailMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillDetailMapper">
|
||||||
|
<!-- 账单明细读写;详情页分页查询,导出任务按游标批量查询。 -->
|
||||||
<resultMap id="ArBillDetailResult" type="ArBillDetail">
|
<resultMap id="ArBillDetailResult" type="ArBillDetail">
|
||||||
<id property="detailId" column="detail_id"/>
|
<id property="detailId" column="detail_id"/>
|
||||||
<result property="arBillId" column="ar_bill_id"/>
|
<result property="arBillId" column="ar_bill_id"/>
|
||||||
@@ -38,6 +39,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
from ar_bill_detail where ar_bill_id = #{arBillId} order by detail_no
|
from ar_bill_detail where ar_bill_id = #{arBillId} order by detail_no
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByBillIdAndSourceIds" resultMap="ArBillDetailResult">
|
||||||
|
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
|
||||||
|
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||||
|
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
|
||||||
|
currency, total_amount, remark, create_by, create_time
|
||||||
|
from ar_bill_detail
|
||||||
|
where ar_bill_id = #{arBillId}
|
||||||
|
and prc_order_bill_id in
|
||||||
|
<foreach collection="sourceBillIds" item="sourceBillId" open="(" separator="," close=")">
|
||||||
|
#{sourceBillId}
|
||||||
|
</foreach>
|
||||||
|
order by detail_no
|
||||||
|
</select>
|
||||||
|
|
||||||
<resultMap id="ArBillDetailWithFeesResult" type="ArBillDetail" extends="ArBillDetailResult">
|
<resultMap id="ArBillDetailWithFeesResult" type="ArBillDetail" extends="ArBillDetailResult">
|
||||||
<collection property="fees" ofType="ArBillDetailFee">
|
<collection property="fees" ofType="ArBillDetailFee">
|
||||||
<id property="detailFeeId" column="fee_detail_id"/>
|
<id property="detailFeeId" column="fee_detail_id"/>
|
||||||
@@ -78,18 +93,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by d.detail_no, d.detail_id, f.fee_no, f.detail_fee_id
|
order by d.detail_no, d.detail_id, f.fee_no, f.detail_fee_id
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectExportPage" resultMap="ArBillDetailResult">
|
||||||
|
select detail_id, ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
|
||||||
|
product_code, product_name, goods_description, country_code, country_name, dest_postcode,
|
||||||
|
actual_weight, volume_weight, charge_weight, pkg_length, pkg_width, pkg_height,
|
||||||
|
currency, total_amount, remark, create_by, create_time
|
||||||
|
from ar_bill_detail
|
||||||
|
where ar_bill_id = #{arBillId} and detail_no > #{lastDetailNo}
|
||||||
|
order by detail_no
|
||||||
|
limit #{pageSize}
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="ArBillDetail" useGeneratedKeys="true" keyProperty="detailId">
|
<insert id="insert" parameterType="ArBillDetail" useGeneratedKeys="true" keyProperty="detailId">
|
||||||
insert into ar_bill_detail (
|
insert into ar_bill_detail (ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time,
|
||||||
ar_bill_id, prc_order_bill_id, detail_no, order_no, tracking_no, receive_time, product_code,
|
product_code,
|
||||||
product_name, goods_description, country_code, country_name, dest_postcode, actual_weight,
|
product_name, goods_description, country_code, country_name, dest_postcode,
|
||||||
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount,
|
actual_weight,
|
||||||
remark, create_by, create_time
|
volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency,
|
||||||
) values (
|
total_amount,
|
||||||
#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime},
|
remark, create_by, create_time)
|
||||||
#{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName},
|
values (#{arBillId}, #{prcOrderBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime},
|
||||||
#{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth},
|
#{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName},
|
||||||
#{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate()
|
#{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth},
|
||||||
)
|
#{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate())
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId">
|
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="detailId">
|
||||||
@@ -107,4 +133,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
#{item.currency}, #{item.totalAmount}, #{item.remark}, #{item.createBy}, sysdate())
|
#{item.currency}, #{item.totalAmount}, #{item.remark}, #{item.createBy}, sysdate())
|
||||||
</foreach>
|
</foreach>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<delete id="deleteByBillId" parameterType="long">
|
||||||
|
delete from ar_bill_detail where ar_bill_id = #{arBillId}
|
||||||
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -3,13 +3,18 @@
|
|||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBillExportMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillExportMapper">
|
||||||
|
<!-- 导出任务和历史记录;PROCESSING/SUCCESS/FAILED 由异步执行器更新。 -->
|
||||||
<resultMap id="ArBillExportResult" type="ArBillExport">
|
<resultMap id="ArBillExportResult" type="ArBillExport">
|
||||||
<id property="exportId" column="export_id"/>
|
<id property="exportId" column="export_id"/>
|
||||||
<result property="arBillId" column="ar_bill_id"/>
|
<result property="arBillId" column="ar_bill_id"/>
|
||||||
<result property="fileName" column="file_name"/>
|
<result property="fileName" column="file_name"/>
|
||||||
<result property="fileType" column="file_type"/>
|
<result property="fileType" column="file_type"/>
|
||||||
|
<result property="templateId" column="template_id"/>
|
||||||
<result property="templateCode" column="template_code"/>
|
<result property="templateCode" column="template_code"/>
|
||||||
<result property="templateVersion" column="template_version"/>
|
<result property="templateVersion" column="template_version"/>
|
||||||
|
<result property="templateStorageType" column="template_storage_type"/>
|
||||||
|
<result property="templateStorageKey" column="template_storage_key"/>
|
||||||
|
<result property="templateHash" column="template_hash"/>
|
||||||
<result property="storageKey" column="storage_key"/>
|
<result property="storageKey" column="storage_key"/>
|
||||||
<result property="fileSize" column="file_size"/>
|
<result property="fileSize" column="file_size"/>
|
||||||
<result property="fileHash" column="file_hash"/>
|
<result property="fileHash" column="file_hash"/>
|
||||||
@@ -21,8 +26,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectExportColumns">
|
<sql id="selectExportColumns">
|
||||||
select export_id, ar_bill_id, file_name, file_type, template_code, template_version,
|
select export_id,
|
||||||
storage_key, file_size, file_hash, row_count, export_status, export_by, export_time, remark
|
ar_bill_id,
|
||||||
|
file_name,
|
||||||
|
file_type,
|
||||||
|
template_id,
|
||||||
|
template_code,
|
||||||
|
template_version,
|
||||||
|
template_storage_type,
|
||||||
|
template_storage_key,
|
||||||
|
template_hash,
|
||||||
|
storage_key,
|
||||||
|
file_size,
|
||||||
|
file_hash,
|
||||||
|
row_count,
|
||||||
|
export_status,
|
||||||
|
export_by,
|
||||||
|
export_time,
|
||||||
|
remark
|
||||||
from ar_bill_export
|
from ar_bill_export
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -40,13 +61,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by export_time desc limit 1
|
order by export_time desc limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectProcessingByBillId" parameterType="long" resultMap="ArBillExportResult">
|
||||||
|
<include refid="selectExportColumns"/>
|
||||||
|
where ar_bill_id = #{arBillId} and export_status = 'PROCESSING'
|
||||||
|
order by export_id desc limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="ArBillExport" useGeneratedKeys="true" keyProperty="exportId">
|
<insert id="insert" parameterType="ArBillExport" useGeneratedKeys="true" keyProperty="exportId">
|
||||||
insert into ar_bill_export (
|
insert into ar_bill_export (
|
||||||
ar_bill_id, file_name, file_type, template_code, template_version, storage_key,
|
ar_bill_id, file_name, file_type, template_id, template_code, template_version,
|
||||||
|
template_storage_type, template_storage_key, template_hash, storage_key,
|
||||||
file_size, file_hash, row_count, export_status, export_by, export_time, remark
|
file_size, file_hash, row_count, export_status, export_by, export_time, remark
|
||||||
) values (
|
) values (
|
||||||
#{arBillId}, #{fileName}, #{fileType}, #{templateCode}, #{templateVersion}, #{storageKey},
|
#{arBillId}, #{fileName}, #{fileType}, #{templateId}, #{templateCode}, #{templateVersion},
|
||||||
|
#{templateStorageType}, #{templateStorageKey}, #{templateHash}, #{storageKey},
|
||||||
#{fileSize}, #{fileHash}, #{rowCount}, #{exportStatus}, #{exportBy}, sysdate(), #{remark}
|
#{fileSize}, #{fileHash}, #{rowCount}, #{exportStatus}, #{exportBy}, sysdate(), #{remark}
|
||||||
)
|
)
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="markSuccess" parameterType="ArBillExport">
|
||||||
|
update ar_bill_export
|
||||||
|
set storage_key = #{storageKey},
|
||||||
|
file_size = #{fileSize},
|
||||||
|
file_hash = #{fileHash},
|
||||||
|
export_status = 'SUCCESS',
|
||||||
|
remark = null
|
||||||
|
where export_id = #{exportId} and export_status = 'PROCESSING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markFailed">
|
||||||
|
update ar_bill_export
|
||||||
|
set export_status = 'FAILED', remark = #{remark}
|
||||||
|
where export_id = #{exportId} and export_status = 'PROCESSING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markExpiredProcessingAsFailed">
|
||||||
|
update ar_bill_export
|
||||||
|
set export_status = 'FAILED', remark = #{remark}
|
||||||
|
where export_status = 'PROCESSING' and export_time < #{expiredBefore}
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillGenerateTaskMapper">
|
||||||
|
<!-- 生成任务表是异步任务的持久化队列;状态更新均带前置状态条件,保证多节点抢占幂等。 -->
|
||||||
|
<resultMap id="ArBillGenerateTaskResult" type="ArBillGenerateTask">
|
||||||
|
<id property="taskId" column="task_id"/>
|
||||||
|
<result property="taskNo" column="task_no"/>
|
||||||
|
<result property="arBillId" column="ar_bill_id"/>
|
||||||
|
<result property="selectionMode" column="selection_mode"/>
|
||||||
|
<result property="requestPayload" column="request_payload"/>
|
||||||
|
<result property="snapshotCreateTime" column="snapshot_create_time"/>
|
||||||
|
<result property="lastSourceBillId" column="last_source_bill_id"/>
|
||||||
|
<result property="totalCount" column="total_count"/>
|
||||||
|
<result property="processedCount" column="processed_count"/>
|
||||||
|
<result property="successCount" column="success_count"/>
|
||||||
|
<result property="totalAmount" column="total_amount"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="errorMessage" column="error_message"/>
|
||||||
|
<result property="requestToken" column="request_token"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="startedAt" column="started_at"/>
|
||||||
|
<result property="heartbeatTime" column="heartbeat_time"/>
|
||||||
|
<result property="finishedAt" column="finished_at"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="columns">
|
||||||
|
select task_id,
|
||||||
|
task_no,
|
||||||
|
ar_bill_id,
|
||||||
|
selection_mode,
|
||||||
|
request_payload,
|
||||||
|
snapshot_create_time,
|
||||||
|
last_source_bill_id,
|
||||||
|
total_count,
|
||||||
|
processed_count,
|
||||||
|
success_count,
|
||||||
|
total_amount,
|
||||||
|
status,
|
||||||
|
error_message,
|
||||||
|
request_token,
|
||||||
|
create_by,
|
||||||
|
create_time,
|
||||||
|
started_at,
|
||||||
|
heartbeat_time,
|
||||||
|
finished_at
|
||||||
|
from ar_bill_generate_task
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" parameterType="long" resultMap="ArBillGenerateTaskResult">
|
||||||
|
<include refid="columns"/>
|
||||||
|
where task_id = #{taskId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByRequestToken" parameterType="string" resultMap="ArBillGenerateTaskResult">
|
||||||
|
<include refid="columns"/>
|
||||||
|
where request_token = #{requestToken}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 前端恢复页面时查询当前用户仍需展示的任务。 -->
|
||||||
|
<select id="selectActiveByCreateBy" resultMap="ArBillGenerateTaskResult">
|
||||||
|
<include refid="columns"/>
|
||||||
|
where create_by = #{createBy}
|
||||||
|
and status in ('WAITING', 'RUNNING', 'CANCEL_REQUESTED')
|
||||||
|
order by create_time desc, task_id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 定时调度器按创建时间顺序取待处理任务。 -->
|
||||||
|
<select id="selectWaiting" resultMap="ArBillGenerateTaskResult">
|
||||||
|
<include refid="columns"/>
|
||||||
|
where status = 'WAITING'
|
||||||
|
order by create_time asc, task_id asc
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="ArBillGenerateTask" useGeneratedKeys="true" keyProperty="taskId">
|
||||||
|
insert into ar_bill_generate_task (task_no, ar_bill_id, selection_mode, request_payload, snapshot_create_time,
|
||||||
|
last_source_bill_id, total_count, processed_count, success_count,
|
||||||
|
total_amount,
|
||||||
|
status, error_message, request_token, create_by, create_time, started_at,
|
||||||
|
heartbeat_time, finished_at)
|
||||||
|
values (#{taskNo}, #{arBillId}, #{selectionMode}, #{requestPayload}, #{snapshotCreateTime},
|
||||||
|
#{lastSourceBillId}, #{totalCount}, #{processedCount}, #{successCount}, #{totalAmount},
|
||||||
|
#{status}, #{errorMessage}, #{requestToken}, #{createBy}, sysdate(), #{startedAt},
|
||||||
|
#{heartbeatTime}, #{finishedAt})
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 条件更新抢占任务;返回 1 才表示当前节点真正拿到执行权。 -->
|
||||||
|
<update id="markRunning">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'RUNNING',
|
||||||
|
started_at = coalesce(started_at, #{startedAt}),
|
||||||
|
heartbeat_time = #{startedAt}
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status = 'WAITING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateTotalCount">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set total_count = #{totalCount},
|
||||||
|
heartbeat_time = sysdate()
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status = 'RUNNING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 每批成功后写入游标、计数和心跳;游标用于故障恢复后继续扫描。 -->
|
||||||
|
<update id="updateProgress">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set last_source_bill_id = #{lastSourceBillId},
|
||||||
|
processed_count = #{processedCount},
|
||||||
|
success_count = #{successCount},
|
||||||
|
total_amount = #{totalAmount},
|
||||||
|
heartbeat_time = #{heartbeatTime}
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status = 'RUNNING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markSuccess">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'SUCCESS',
|
||||||
|
finished_at = #{finishedAt},
|
||||||
|
heartbeat_time = #{finishedAt}
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status = 'RUNNING'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markFailed">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'FAILED',
|
||||||
|
error_message = #{errorMessage},
|
||||||
|
finished_at = #{finishedAt},
|
||||||
|
heartbeat_time = #{finishedAt}
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status in ('WAITING', 'RUNNING', 'CANCEL_REQUESTED')
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markCancelRequested">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'CANCEL_REQUESTED',
|
||||||
|
heartbeat_time = sysdate()
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status in ('WAITING', 'RUNNING')
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markCanceled">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'CANCELED',
|
||||||
|
error_message = '用户取消账单生成',
|
||||||
|
finished_at = #{finishedAt},
|
||||||
|
heartbeat_time = #{finishedAt}
|
||||||
|
where task_id = #{taskId}
|
||||||
|
and status in ('WAITING', 'RUNNING', 'CANCEL_REQUESTED')
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 执行节点宕机时恢复无心跳的 RUNNING 任务,供调度器重新投递。 -->
|
||||||
|
<update id="resetExpiredRunning">
|
||||||
|
update ar_bill_generate_task
|
||||||
|
set status = 'WAITING',
|
||||||
|
error_message = '任务执行节点已重启,已自动恢复等待处理',
|
||||||
|
heartbeat_time = null
|
||||||
|
where status = 'RUNNING'
|
||||||
|
and heartbeat_time < #{expiredBefore}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 取消请求长时间未完成时由补偿任务取出并执行数据清理。 -->
|
||||||
|
<select id="selectCancelRequestedBefore" resultMap="ArBillGenerateTaskResult">
|
||||||
|
<include refid="columns"/>
|
||||||
|
where status = 'CANCEL_REQUESTED'
|
||||||
|
and coalesce(heartbeat_time, create_time) < #{expiredBefore}
|
||||||
|
order by create_time asc, task_id asc
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBillLogMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillLogMapper">
|
||||||
<resultMap id="ArBillLogResult" type="ArBillLog">
|
<resultMap id="ArBillLogResult" type="ArBillLog">
|
||||||
<id property="logId" column="log_id"/>
|
<id property="logId" column="log_id"/>
|
||||||
@@ -28,24 +28,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<select id="selectList" parameterType="ArBillLog" resultMap="ArBillLogResult">
|
<select id="selectList" parameterType="ArBillLog" resultMap="ArBillLogResult">
|
||||||
select l.log_id, l.ar_bill_id, l.object_type, l.object_id, l.operation_type, l.operation_desc,
|
select l.log_id, l.ar_bill_id, l.object_type, l.object_id, l.operation_type, l.operation_desc,
|
||||||
case l.object_type
|
case l.object_type
|
||||||
when 'BILL' then b.bill_no
|
when 'BILL' then b.bill_no
|
||||||
when 'EXPORT' then coalesce(e.file_name, b.bill_no)
|
when 'EXPORT' then coalesce(e.file_name, b.bill_no)
|
||||||
when 'BALANCE_WRITEOFF' then bw.writeoff_no
|
when 'BALANCE_WRITEOFF' then bw.writeoff_no
|
||||||
else null
|
else null
|
||||||
end as object_display,
|
end as object_display,
|
||||||
l.from_status, l.to_status, l.before_data, l.after_data, l.operator_id, l.operator_name,
|
l.from_status, l.to_status, l.before_data, l.after_data, l.operator_id, l.operator_name,
|
||||||
l.operation_source, l.request_id, l.trace_id, l.result, l.error_message, l.client_ip, l.create_time
|
l.operation_source, l.request_id, l.trace_id, l.result, l.error_message, l.client_ip, l.create_time
|
||||||
from ar_bill_log l
|
from ar_bill_log l
|
||||||
left join ar_bill b on b.ar_bill_id = l.ar_bill_id
|
left join ar_bill b on b.ar_bill_id = l.ar_bill_id
|
||||||
left join ar_bill_export e on e.export_id = case when l.object_type = 'EXPORT' then cast(l.object_id as unsigned) end
|
left join ar_bill_export e on e.export_id = case when l.object_type = 'EXPORT' then cast(l.object_id as
|
||||||
left join ar_balance_writeoff bw on bw.writeoff_id = case when l.object_type = 'BALANCE_WRITEOFF' then cast(l.object_id as unsigned) end
|
unsigned) end
|
||||||
|
left join ar_balance_writeoff bw on bw.writeoff_id = case when l.object_type = 'BALANCE_WRITEOFF' then
|
||||||
|
cast(l.object_id as unsigned) end
|
||||||
<where>
|
<where>
|
||||||
<if test="arBillId != null">and l.ar_bill_id = #{arBillId}</if>
|
<if test="arBillId != null">and l.ar_bill_id = #{arBillId}</if>
|
||||||
<if test="objectType != null and objectType != ''">and l.object_type = #{objectType}</if>
|
<if test="objectType != null and objectType != ''">and l.object_type = #{objectType}</if>
|
||||||
<if test="objectId != null and objectId != ''">and l.object_id = #{objectId}</if>
|
<if test="objectId != null and objectId != ''">and l.object_id = #{objectId}</if>
|
||||||
<if test="operationType != null and operationType != ''">and l.operation_type = #{operationType}</if>
|
<if test="operationType != null and operationType != ''">and l.operation_type = #{operationType}</if>
|
||||||
<if test="operationSource != null and operationSource != ''">and l.operation_source = #{operationSource}</if>
|
<if test="operationSource != null and operationSource != ''">and l.operation_source = #{operationSource}
|
||||||
|
</if>
|
||||||
<if test="requestId != null and requestId != ''">and l.request_id = #{requestId}</if>
|
<if test="requestId != null and requestId != ''">and l.request_id = #{requestId}</if>
|
||||||
<if test="result != null and result != ''">and l.result = #{result}</if>
|
<if test="result != null and result != ''">and l.result = #{result}</if>
|
||||||
</where>
|
</where>
|
||||||
@@ -53,23 +56,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectSuccessByRequestId" resultMap="ArBillLogResult">
|
<select id="selectSuccessByRequestId" resultMap="ArBillLogResult">
|
||||||
select log_id, ar_bill_id, object_type, object_id, operation_type, operation_desc,
|
select log_id,
|
||||||
from_status, to_status, before_data, after_data, operator_id, operator_name,
|
ar_bill_id,
|
||||||
operation_source, request_id, trace_id, result, error_message, client_ip, create_time
|
object_type,
|
||||||
|
object_id,
|
||||||
|
operation_type,
|
||||||
|
operation_desc,
|
||||||
|
from_status,
|
||||||
|
to_status,
|
||||||
|
before_data,
|
||||||
|
after_data,
|
||||||
|
operator_id,
|
||||||
|
operator_name,
|
||||||
|
operation_source,
|
||||||
|
request_id,
|
||||||
|
trace_id,
|
||||||
|
result,
|
||||||
|
error_message,
|
||||||
|
client_ip,
|
||||||
|
create_time
|
||||||
from ar_bill_log
|
from ar_bill_log
|
||||||
where request_id = #{requestId} and object_type = #{objectType} and result = #{result}
|
where request_id = #{requestId}
|
||||||
|
and object_type = #{objectType}
|
||||||
|
and result = #{result}
|
||||||
order by log_id limit 1
|
order by log_id limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="ArBillLog" useGeneratedKeys="true" keyProperty="logId">
|
<insert id="insert" parameterType="ArBillLog" useGeneratedKeys="true" keyProperty="logId">
|
||||||
insert into ar_bill_log (
|
insert into ar_bill_log (ar_bill_id, object_type, object_id, operation_type, operation_desc, from_status,
|
||||||
ar_bill_id, object_type, object_id, operation_type, operation_desc, from_status, to_status,
|
to_status,
|
||||||
before_data, after_data, operator_id, operator_name, operation_source, request_id,
|
before_data, after_data, operator_id, operator_name, operation_source, request_id,
|
||||||
trace_id, result, error_message, client_ip, create_time
|
trace_id, result, error_message, client_ip, create_time)
|
||||||
) values (
|
values (#{arBillId}, #{objectType}, #{objectId}, #{operationType}, #{operationDesc}, #{fromStatus}, #{toStatus},
|
||||||
#{arBillId}, #{objectType}, #{objectId}, #{operationType}, #{operationDesc}, #{fromStatus}, #{toStatus},
|
#{beforeData}, #{afterData}, #{operatorId}, #{operatorName}, #{operationSource}, #{requestId},
|
||||||
#{beforeData}, #{afterData}, #{operatorId}, #{operatorName}, #{operationSource}, #{requestId},
|
#{traceId}, #{result}, #{errorMessage}, #{clientIp}, sysdate())
|
||||||
#{traceId}, #{result}, #{errorMessage}, #{clientIp}, sysdate()
|
|
||||||
)
|
|
||||||
</insert>
|
</insert>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.ar.mapper.ArBillMapper">
|
<mapper namespace="com.ruoyi.ar.mapper.ArBillMapper">
|
||||||
|
<!-- 账单头查询、状态流转和金额/生成进度更新。带 ForUpdate 的查询用于核销并发控制。 -->
|
||||||
<resultMap id="ArBillResult" type="ArBill">
|
<resultMap id="ArBillResult" type="ArBill">
|
||||||
<id property="arBillId" column="ar_bill_id"/>
|
<id property="arBillId" column="ar_bill_id"/>
|
||||||
<result property="billNo" column="bill_no"/>
|
<result property="billNo" column="bill_no"/>
|
||||||
@@ -19,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="writtenOffAmount" column="written_off_amount"/>
|
<result property="writtenOffAmount" column="written_off_amount"/>
|
||||||
<result property="unwrittenAmount" column="unwritten_amount"/>
|
<result property="unwrittenAmount" column="unwritten_amount"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
|
<result property="generationStatus" column="generation_status"/>
|
||||||
<result property="billVersion" column="bill_version"/>
|
<result property="billVersion" column="bill_version"/>
|
||||||
<result property="bankAccountName" column="bank_account_name"/>
|
<result property="bankAccountName" column="bank_account_name"/>
|
||||||
<result property="bankAccountNo" column="bank_account_no"/>
|
<result property="bankAccountNo" column="bank_account_no"/>
|
||||||
@@ -33,21 +35,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectArBillColumns">
|
<sql id="selectArBillColumns">
|
||||||
select ar_bill_id, bill_no, bill_name, customer_id, customer_code, customer_name,
|
select ar_bill_id,
|
||||||
bill_period_start, bill_period_end, bill_date, currency, order_count,
|
bill_no,
|
||||||
total_amount, written_off_amount, unwritten_amount, status, bill_version,
|
bill_name,
|
||||||
bank_account_name, bank_account_no, bank_name, remark, confirmed_by,
|
customer_id,
|
||||||
confirmed_time, create_by, create_time, update_by, update_time
|
customer_code,
|
||||||
|
customer_name,
|
||||||
|
bill_period_start,
|
||||||
|
bill_period_end,
|
||||||
|
bill_date,
|
||||||
|
currency,
|
||||||
|
order_count,
|
||||||
|
total_amount,
|
||||||
|
written_off_amount,
|
||||||
|
unwritten_amount,
|
||||||
|
status,
|
||||||
|
generation_status,
|
||||||
|
bill_version,
|
||||||
|
bank_account_name,
|
||||||
|
bank_account_no,
|
||||||
|
bank_name,
|
||||||
|
remark,
|
||||||
|
confirmed_by,
|
||||||
|
confirmed_time,
|
||||||
|
create_by,
|
||||||
|
create_time,
|
||||||
|
update_by,
|
||||||
|
update_time
|
||||||
from ar_bill
|
from ar_bill
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectArBillList" parameterType="com.ruoyi.ar.domain.vo.ArBillQuery" resultMap="ArBillResult">
|
<select id="selectArBillList" parameterType="com.ruoyi.ar.domain.vo.ArBillQuery" resultMap="ArBillResult">
|
||||||
<include refid="selectArBillColumns"/>
|
<include refid="selectArBillColumns"/>
|
||||||
<where>
|
<where>
|
||||||
|
and (generation_status is null or generation_status = 'READY')
|
||||||
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
|
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
|
||||||
<if test="billName != null and billName != ''">and bill_name like concat('%', #{billName}, '%')</if>
|
<if test="billName != null and billName != ''">and bill_name like concat('%', #{billName}, '%')</if>
|
||||||
<if test="customerCode != null and customerCode != ''">and customer_code like concat('%', #{customerCode}, '%')</if>
|
<if test="customerCode != null and customerCode != ''">and customer_code like concat('%', #{customerCode},
|
||||||
<if test="customerName != null and customerName != ''">and customer_name like concat('%', #{customerName}, '%')</if>
|
'%')
|
||||||
|
</if>
|
||||||
|
<if test="customerName != null and customerName != ''">and customer_name like concat('%', #{customerName},
|
||||||
|
'%')
|
||||||
|
</if>
|
||||||
<if test="customerId != null">and customer_id = #{customerId}</if>
|
<if test="customerId != null">and customer_id = #{customerId}</if>
|
||||||
<if test="status != null and status != ''">and status = #{status}</if>
|
<if test="status != null and status != ''">and status = #{status}</if>
|
||||||
<if test="currency != null and currency != ''">and currency = #{currency}</if>
|
<if test="currency != null and currency != ''">and currency = #{currency}</if>
|
||||||
@@ -59,11 +88,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectArBillById" parameterType="long" resultMap="ArBillResult">
|
<select id="selectArBillById" parameterType="long" resultMap="ArBillResult">
|
||||||
<include refid="selectArBillColumns"/> where ar_bill_id = #{arBillId}
|
<include refid="selectArBillColumns"/>
|
||||||
|
where ar_bill_id = #{arBillId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectArBillByBillNo" parameterType="string" resultMap="ArBillResult">
|
<select id="selectArBillByBillNo" parameterType="string" resultMap="ArBillResult">
|
||||||
<include refid="selectArBillColumns"/> where bill_no = #{billNo}
|
<include refid="selectArBillColumns"/>
|
||||||
|
where bill_no = #{billNo}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectGeneratingByCustomerPeriod" resultMap="ArBillResult">
|
||||||
|
<include refid="selectArBillColumns"/>
|
||||||
|
where customer_id = #{customerId}
|
||||||
|
and currency = #{currency}
|
||||||
|
and bill_period_end = #{cutoffDate}
|
||||||
|
and generation_status = 'GENERATING'
|
||||||
|
order by ar_bill_id desc
|
||||||
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectMaxBillVersion" resultType="int">
|
<select id="selectMaxBillVersion" resultType="int">
|
||||||
@@ -72,41 +113,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
where customer_id = #{customerId}
|
where customer_id = #{customerId}
|
||||||
and bill_period_end = #{cutoffDate}
|
and bill_period_end = #{cutoffDate}
|
||||||
and currency = #{currency}
|
and currency = #{currency}
|
||||||
order by bill_version desc
|
order by bill_version desc limit 1
|
||||||
limit 1
|
for
|
||||||
for update
|
update
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectArBillByIdForUpdate" parameterType="long" resultMap="ArBillResult">
|
<select id="selectArBillByIdForUpdate" parameterType="long" resultMap="ArBillResult">
|
||||||
<include refid="selectArBillColumns"/> where ar_bill_id = #{arBillId} for update
|
<include refid="selectArBillColumns"/>
|
||||||
|
where ar_bill_id = #{arBillId} for update
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectUnsettledByCustomer" resultMap="ArBillResult">
|
<select id="selectUnsettledByCustomer" resultMap="ArBillResult">
|
||||||
<include refid="selectArBillColumns"/>
|
<include refid="selectArBillColumns"/>
|
||||||
where customer_id = #{customerId}
|
where customer_id = #{customerId}
|
||||||
and status in ('CONFIRMED', 'PARTIAL_WRITTEN_OFF')
|
and status in ('CONFIRMED', 'PARTIAL_WRITTEN_OFF')
|
||||||
and unwritten_amount > 0
|
and (generation_status is null or generation_status = 'READY')
|
||||||
|
and unwritten_amount > 0
|
||||||
order by bill_date asc, ar_bill_id asc
|
order by bill_date asc, ar_bill_id asc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertArBill" parameterType="ArBill" useGeneratedKeys="true" keyProperty="arBillId">
|
<insert id="insertArBill" parameterType="ArBill" useGeneratedKeys="true" keyProperty="arBillId">
|
||||||
insert into ar_bill (
|
insert into ar_bill (bill_no, bill_name, customer_id, customer_code, customer_name, bill_period_start,
|
||||||
bill_no, bill_name, customer_id, customer_code, customer_name, bill_period_start, bill_period_end,
|
bill_period_end,
|
||||||
bill_date, currency, order_count, total_amount, written_off_amount, unwritten_amount,
|
bill_date, currency, order_count, total_amount, written_off_amount, unwritten_amount,
|
||||||
status, bill_version, bank_account_name, bank_account_no, bank_name, remark,
|
status, generation_status, bill_version, bank_account_name, bank_account_no, bank_name,
|
||||||
confirmed_by, confirmed_time, create_by, create_time, update_by, update_time
|
remark,
|
||||||
) values (
|
confirmed_by, confirmed_time, create_by, create_time, update_by, update_time)
|
||||||
#{billNo}, #{billName}, #{customerId}, #{customerCode}, #{customerName}, #{billPeriodStart}, #{billPeriodEnd},
|
values (#{billNo}, #{billName}, #{customerId}, #{customerCode}, #{customerName}, #{billPeriodStart},
|
||||||
#{billDate}, #{currency}, #{orderCount}, #{totalAmount}, #{writtenOffAmount}, #{unwrittenAmount},
|
#{billPeriodEnd},
|
||||||
#{status}, #{billVersion}, #{bankAccountName}, #{bankAccountNo}, #{bankName}, #{remark},
|
#{billDate}, #{currency}, #{orderCount}, #{totalAmount}, #{writtenOffAmount}, #{unwrittenAmount},
|
||||||
#{confirmedBy}, #{confirmedTime}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
#{status}, #{generationStatus}, #{billVersion}, #{bankAccountName}, #{bankAccountNo}, #{bankName},
|
||||||
)
|
#{remark},
|
||||||
|
#{confirmedBy}, #{confirmedTime}, #{createBy}, sysdate(), #{updateBy}, sysdate())
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="updateArBillStatus">
|
<update id="updateArBillStatus">
|
||||||
update ar_bill
|
update ar_bill
|
||||||
set status = #{status}, remark = #{remark}, update_by = #{operator}, update_time = sysdate(),
|
set status = #{status},
|
||||||
confirmed_by = case when #{status} = 'CONFIRMED' then #{operator} else confirmed_by end,
|
remark = #{remark},
|
||||||
|
update_by = #{operator},
|
||||||
|
update_time = sysdate(),
|
||||||
|
confirmed_by = case when #{status} = 'CONFIRMED' then #{operator} else confirmed_by end,
|
||||||
confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end
|
confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end
|
||||||
where ar_bill_id = #{arBillId}
|
where ar_bill_id = #{arBillId}
|
||||||
and status = #{fromStatus}
|
and status = #{fromStatus}
|
||||||
@@ -114,9 +161,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<update id="updateArBillAmounts">
|
<update id="updateArBillAmounts">
|
||||||
update ar_bill
|
update ar_bill
|
||||||
set written_off_amount = #{writtenOffAmount}, unwritten_amount = #{unwrittenAmount},
|
set written_off_amount = #{writtenOffAmount},
|
||||||
status = #{status}, update_by = #{operator}, update_time = sysdate()
|
unwritten_amount = #{unwrittenAmount},
|
||||||
|
status = #{status},
|
||||||
|
update_by = #{operator},
|
||||||
|
update_time = sysdate()
|
||||||
where ar_bill_id = #{arBillId}
|
where ar_bill_id = #{arBillId}
|
||||||
and status = #{fromStatus}
|
and status = #{fromStatus}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<update id="updateArBillProgress">
|
||||||
|
update ar_bill
|
||||||
|
set order_count = #{orderCount},
|
||||||
|
total_amount = #{totalAmount},
|
||||||
|
unwritten_amount = #{unwrittenAmount},
|
||||||
|
bill_period_start = case
|
||||||
|
when #{billPeriodStart} < bill_period_start then #{billPeriodStart}
|
||||||
|
else bill_period_start end,
|
||||||
|
update_by = #{operator},
|
||||||
|
update_time = sysdate()
|
||||||
|
where ar_bill_id = #{arBillId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateArBillGenerationStatus">
|
||||||
|
update ar_bill
|
||||||
|
set generation_status = #{generationStatus},
|
||||||
|
update_by = #{operator},
|
||||||
|
update_time = sysdate(),
|
||||||
|
remark = case when #{remark} is null or #{remark} = '' then remark else #{remark} end
|
||||||
|
where ar_bill_id = #{arBillId}
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper
|
||||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.ar.mapper.ArExportTemplateMapper">
|
||||||
|
<!-- 模板管理查询;导出提交时会把选中的模板版本快照到导出任务。 -->
|
||||||
|
<resultMap id="ArExportTemplateResult" type="ArExportTemplate">
|
||||||
|
<id property="templateId" column="template_id"/>
|
||||||
|
<result property="templateCode" column="template_code"/>
|
||||||
|
<result property="templateName" column="template_name"/>
|
||||||
|
<result property="businessType" column="business_type"/>
|
||||||
|
<result property="templateVersion" column="template_version"/>
|
||||||
|
<result property="storageType" column="storage_type"/>
|
||||||
|
<result property="storageKey" column="storage_key"/>
|
||||||
|
<result property="fileHash" column="file_hash"/>
|
||||||
|
<result property="status" column="status"/>
|
||||||
|
<result property="isDefault" column="is_default"/>
|
||||||
|
<result property="effectiveStart" column="effective_start"/>
|
||||||
|
<result property="effectiveEnd" column="effective_end"/>
|
||||||
|
<result property="remark" column="remark"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectDefaultEnabled" resultMap="ArExportTemplateResult">
|
||||||
|
select template_id,
|
||||||
|
template_code,
|
||||||
|
template_name,
|
||||||
|
business_type,
|
||||||
|
template_version,
|
||||||
|
storage_type,
|
||||||
|
storage_key,
|
||||||
|
file_hash,
|
||||||
|
status,
|
||||||
|
is_default,
|
||||||
|
effective_start,
|
||||||
|
effective_end,
|
||||||
|
remark
|
||||||
|
from ar_export_template
|
||||||
|
where business_type = #{businessType}
|
||||||
|
and status = 'ENABLED'
|
||||||
|
and is_default = '1'
|
||||||
|
and (effective_start is null or effective_start <= #{effectiveDate})
|
||||||
|
and (effective_end is null or effective_end >= #{effectiveDate})
|
||||||
|
order by effective_start desc, template_id desc limit 1
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -4,6 +4,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.price.mapper.PrcOrderBillMapper">
|
<mapper namespace="com.ruoyi.price.mapper.PrcOrderBillMapper">
|
||||||
|
|
||||||
|
<!-- 应收账单生成依赖的来源计费订单查询;本项目的 prc_bill 需求表映射为 prc_order_bill。 -->
|
||||||
|
|
||||||
<resultMap type="PrcOrderBill" id="PrcOrderBillResult">
|
<resultMap type="PrcOrderBill" id="PrcOrderBillResult">
|
||||||
<id property="billId" column="bill_id" />
|
<id property="billId" column="bill_id" />
|
||||||
<result property="billKey" column="bill_key" />
|
<result property="billKey" column="bill_key" />
|
||||||
@@ -155,21 +157,107 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by create_time desc, bill_id desc
|
order by create_time desc, bill_id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- The repository's rename_bill_tables.sql maps the requirement's prc_bill to prc_order_bill. -->
|
<!-- rename_bill_tables.sql 已将需求文档中的 prc_bill 映射为本项目的 prc_order_bill。 -->
|
||||||
|
<!-- 生成弹窗预览:按客户、截止日、币种和订单/运单号 OR 条件分页查询未入账订单。 -->
|
||||||
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillResult">
|
<select id="selectPrcOrderBillByCustomerPeriod" resultMap="PrcOrderBillResult">
|
||||||
<include refid="selectPrcOrderBillVo"/>
|
<include refid="selectPrcOrderBillVo"/>
|
||||||
where customer_id = #{customerId}
|
where customer_id = #{customerId}
|
||||||
<if test="customerCode != null and customerCode != ''">
|
<if test="customerCode != null and customerCode != ''">
|
||||||
and customer_code like concat('%', #{customerCode}, '%')
|
and customer_code like concat('%', #{customerCode}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
|
||||||
</if>
|
</if>
|
||||||
and biz_date <= #{cutoffDate}
|
and biz_date <= #{cutoffDate}
|
||||||
and ar_bill_status = #{arBillStatus}
|
and ar_bill_status = #{arBillStatus}
|
||||||
<if test="currency != null and currency != ''">
|
<if test="currency != null and currency != ''">
|
||||||
and currency = #{currency}
|
and currency = #{currency}
|
||||||
</if>
|
</if>
|
||||||
|
<if test="excludedBillIds != null and excludedBillIds.size() > 0">
|
||||||
|
and bill_id not in
|
||||||
|
<foreach collection="excludedBillIds" item="excludedBillId" open="(" separator="," close=")">
|
||||||
|
#{excludedBillId}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
order by biz_date, order_no, bill_id
|
order by biz_date, order_no, bill_id
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="sumPrcOrderBillIncomeByCustomerPeriod" resultType="java.math.BigDecimal">
|
||||||
|
select coalesce(sum(income_total), 0)
|
||||||
|
from prc_order_bill
|
||||||
|
where customer_id = #{customerId}
|
||||||
|
<if test="customerCode != null and customerCode != ''">
|
||||||
|
and customer_code like concat('%', #{customerCode}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="excludeOrderOrTrackingNo != null and excludeOrderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{excludeOrderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{excludeOrderOrTrackingNo}, '%'))
|
||||||
|
</if>
|
||||||
|
and biz_date <= #{cutoffDate}
|
||||||
|
and ar_bill_status = #{arBillStatus}
|
||||||
|
<if test="currency != null and currency != ''">
|
||||||
|
and currency = #{currency}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<sql id="customerPeriodConditions">
|
||||||
|
where customer_id = #{customerId}
|
||||||
|
<if test="customerCode != null and customerCode != ''">
|
||||||
|
and customer_code like concat('%', #{customerCode}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
|
||||||
|
</if>
|
||||||
|
and biz_date <= #{cutoffDate}
|
||||||
|
and ar_bill_status = #{arBillStatus}
|
||||||
|
<if test="currency != null and currency != ''">
|
||||||
|
and currency = #{currency}
|
||||||
|
</if>
|
||||||
|
<if test="snapshotCreateTime != null">
|
||||||
|
and (create_time is null or create_time <= #{snapshotCreateTime})
|
||||||
|
</if>
|
||||||
|
<if test="billIds != null and billIds.size() > 0">
|
||||||
|
and bill_id in
|
||||||
|
<foreach collection="billIds" item="billId" open="(" separator="," close=")">
|
||||||
|
#{billId}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="excludedBillIds != null and excludedBillIds.size() > 0">
|
||||||
|
and bill_id not in
|
||||||
|
<foreach collection="excludedBillIds" item="excludedBillId" open="(" separator="," close=")">
|
||||||
|
#{excludedBillId}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<!-- 异步生成使用 bill_id 游标批量读取,避免深分页和一次加载全量订单。 -->
|
||||||
|
<select id="selectPrcOrderBillByCustomerPeriodCursor" resultMap="PrcOrderBillResult">
|
||||||
|
<include refid="selectPrcOrderBillVo"/>
|
||||||
|
<include refid="customerPeriodConditions"/>
|
||||||
|
<if test="lastSourceBillId != null and lastSourceBillId != ''">
|
||||||
|
and bill_id > #{lastSourceBillId}
|
||||||
|
</if>
|
||||||
|
order by bill_id
|
||||||
|
limit #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 全选模式先统计总数,用于任务进度和空范围校验。 -->
|
||||||
|
<select id="countPrcOrderBillByCustomerPeriod" resultType="int">
|
||||||
|
select count(1)
|
||||||
|
from prc_order_bill
|
||||||
|
<include refid="customerPeriodConditions"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectPrcOrderBillByCustomerCutoffAndIds" resultMap="PrcOrderBillResult">
|
<select id="selectPrcOrderBillByCustomerCutoffAndIds" resultMap="PrcOrderBillResult">
|
||||||
<include refid="selectPrcOrderBillVo"/>
|
<include refid="selectPrcOrderBillVo"/>
|
||||||
where customer_id = #{customerId}
|
where customer_id = #{customerId}
|
||||||
@@ -182,6 +270,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="customerCode != null and customerCode != ''">
|
<if test="customerCode != null and customerCode != ''">
|
||||||
and customer_code like concat('%', #{customerCode}, '%')
|
and customer_code like concat('%', #{customerCode}, '%')
|
||||||
</if>
|
</if>
|
||||||
|
<if test="orderOrTrackingNo != null and orderOrTrackingNo != ''">
|
||||||
|
and (order_no like concat('%', #{orderOrTrackingNo}, '%')
|
||||||
|
or tracking_no like concat('%', #{orderOrTrackingNo}, '%'))
|
||||||
|
</if>
|
||||||
<if test="currency != null and currency != ''">
|
<if test="currency != null and currency != ''">
|
||||||
and currency = #{currency}
|
and currency = #{currency}
|
||||||
</if>
|
</if>
|
||||||
@@ -280,6 +372,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
where bill_id = #{billId}
|
where bill_id = #{billId}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<!-- 抢占来源订单:只有未入账订单可以变更为已入账,防止重复生成。 -->
|
||||||
<update id="claimArBillOrders">
|
<update id="claimArBillOrders">
|
||||||
update prc_order_bill
|
update prc_order_bill
|
||||||
set ar_bill_status = #{toStatus}, updated_by = #{operator}, update_time = sysdate()
|
set ar_bill_status = #{toStatus}, updated_by = #{operator}, update_time = sysdate()
|
||||||
@@ -299,4 +392,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
#{billId}
|
#{billId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<update id="releaseArBillOrdersByArBillId">
|
||||||
|
update prc_order_bill p
|
||||||
|
inner join ar_bill_detail d on d.prc_order_bill_id = p.bill_id
|
||||||
|
set p.ar_bill_status = #{toStatus}, p.updated_by = #{operator}, p.update_time = sysdate()
|
||||||
|
where d.ar_bill_id = #{arBillId}
|
||||||
|
and p.ar_bill_status = #{fromStatus}
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS ar_bill (
|
|||||||
written_off_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '已核销金额缓存',
|
written_off_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '已核销金额缓存',
|
||||||
unwritten_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '未核销金额缓存',
|
unwritten_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '未核销金额缓存',
|
||||||
status VARCHAR(30) NOT NULL COMMENT '状态:DRAFT=草稿,CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销,VOID=已作废',
|
status VARCHAR(30) NOT NULL COMMENT '状态:DRAFT=草稿,CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销,VOID=已作废',
|
||||||
|
generation_status VARCHAR(20) NOT NULL DEFAULT 'READY' COMMENT '生成状态:GENERATING=生成中,READY=可用,FAILED=生成失败',
|
||||||
bill_version INT(11) NOT NULL DEFAULT 1 COMMENT '账单版本',
|
bill_version INT(11) NOT NULL DEFAULT 1 COMMENT '账单版本',
|
||||||
bank_account_name VARCHAR(100) DEFAULT NULL COMMENT '收款账户名称快照',
|
bank_account_name VARCHAR(100) DEFAULT NULL COMMENT '收款账户名称快照',
|
||||||
bank_account_no VARCHAR(128) DEFAULT NULL COMMENT '收款账号快照,应用层脱敏展示',
|
bank_account_no VARCHAR(128) DEFAULT NULL COMMENT '收款账号快照,应用层脱敏展示',
|
||||||
@@ -40,6 +41,34 @@ CREATE TABLE IF NOT EXISTS ar_bill (
|
|||||||
KEY idx_ar_bill_status (status)
|
KEY idx_ar_bill_status (status)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='正式应收账单主表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='正式应收账单主表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ar_bill_generate_task (
|
||||||
|
task_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '生成任务ID',
|
||||||
|
task_no VARCHAR(50) NOT NULL COMMENT '生成任务编号',
|
||||||
|
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||||
|
selection_mode VARCHAR(20) NOT NULL COMMENT '选择模式:EXPLICIT=手动选择,ALL_MATCHED=全选查询结果',
|
||||||
|
request_payload LONGTEXT NOT NULL COMMENT '生成请求快照JSON',
|
||||||
|
snapshot_create_time DATETIME DEFAULT NULL COMMENT '来源订单创建时间快照',
|
||||||
|
last_source_bill_id VARCHAR(36) DEFAULT NULL COMMENT '来源订单游标',
|
||||||
|
total_count INT(11) NOT NULL DEFAULT -1 COMMENT '待处理订单数,-1表示尚未统计',
|
||||||
|
processed_count INT(11) NOT NULL DEFAULT 0 COMMENT '已处理订单数',
|
||||||
|
success_count INT(11) NOT NULL DEFAULT 0 COMMENT '成功订单数',
|
||||||
|
total_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当前生成金额',
|
||||||
|
status VARCHAR(30) NOT NULL COMMENT '任务状态:WAITING=待处理,RUNNING=生成中,SUCCESS=生成成功,FAILED=生成失败,CANCEL_REQUESTED=取消中,CANCELED=已取消',
|
||||||
|
error_message VARCHAR(1000) DEFAULT NULL COMMENT '错误信息',
|
||||||
|
request_token VARCHAR(64) NOT NULL COMMENT '请求幂等标识',
|
||||||
|
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||||
|
started_at DATETIME DEFAULT NULL COMMENT '开始时间',
|
||||||
|
heartbeat_time DATETIME DEFAULT NULL COMMENT '最近心跳时间',
|
||||||
|
finished_at DATETIME DEFAULT NULL COMMENT '完成时间',
|
||||||
|
PRIMARY KEY (task_id),
|
||||||
|
UNIQUE KEY uk_ar_bill_generate_task_no (task_no),
|
||||||
|
UNIQUE KEY uk_ar_bill_generate_request_token (request_token),
|
||||||
|
KEY idx_ar_bill_generate_status (status, create_time),
|
||||||
|
KEY idx_ar_bill_generate_creator (create_by, status, create_time),
|
||||||
|
KEY idx_ar_bill_generate_bill (ar_bill_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单异步生成任务';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ar_bill_detail (
|
CREATE TABLE IF NOT EXISTS ar_bill_detail (
|
||||||
detail_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '账单订单明细ID',
|
detail_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '账单订单明细ID',
|
||||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||||
@@ -98,13 +127,54 @@ CREATE TABLE IF NOT EXISTS ar_bill_detail_fee (
|
|||||||
CONSTRAINT fk_ar_detail_fee_detail FOREIGN KEY (detail_id) REFERENCES ar_bill_detail (detail_id)
|
CONSTRAINT fk_ar_detail_fee_detail FOREIGN KEY (detail_id) REFERENCES ar_bill_detail (detail_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单费用快照';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单费用快照';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ar_export_template (
|
||||||
|
template_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出模板ID',
|
||||||
|
template_code VARCHAR(50) NOT NULL COMMENT '模板编码,例如 AR_CUSTOMER_BILL',
|
||||||
|
template_name VARCHAR(100) NOT NULL COMMENT '模板名称',
|
||||||
|
business_type VARCHAR(50) NOT NULL COMMENT '业务类型,例如 AR_BILL',
|
||||||
|
template_version VARCHAR(50) NOT NULL COMMENT '模板版本,已启用版本不可覆盖',
|
||||||
|
storage_type VARCHAR(20) NOT NULL DEFAULT 'CLASSPATH' COMMENT '存储类型:CLASSPATH=应用内置资源,FILE=服务器文件,OSS=对象存储,MINIO=MinIO对象存储',
|
||||||
|
storage_key VARCHAR(500) NOT NULL COMMENT '模板文件存储Key或classpath资源路径',
|
||||||
|
file_hash VARCHAR(128) DEFAULT NULL COMMENT '模板文件SHA-256校验值',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED=启用,DISABLED=停用',
|
||||||
|
is_default CHAR(1) NOT NULL DEFAULT '0' COMMENT '是否默认模板:1是0否',
|
||||||
|
effective_start DATE DEFAULT NULL COMMENT '生效开始日期',
|
||||||
|
effective_end DATE DEFAULT NULL COMMENT '生效结束日期',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
create_by VARCHAR(64) NOT NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (template_id),
|
||||||
|
UNIQUE KEY uk_ar_export_template_code_version (business_type, template_code, template_version),
|
||||||
|
KEY idx_ar_export_template_default (business_type, status, is_default, effective_start, effective_end)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单导出模板管理';
|
||||||
|
|
||||||
|
INSERT INTO ar_export_template (
|
||||||
|
template_code, template_name, business_type, template_version, storage_type, storage_key,
|
||||||
|
file_hash, status, is_default, create_by, create_time, remark
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'AR_CUSTOMER_BILL', '应收账单默认模板', 'AR_BILL', '1.0', 'CLASSPATH',
|
||||||
|
'templates/ar/ar_bill_template.xlsx', '735002ac3561e0579b724c0612a2c5ce2d6725bc7fd39c450b0aeff70911a45e', 'ENABLED', '1', 'system', NOW(), '系统内置默认模板'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM ar_export_template
|
||||||
|
WHERE business_type = 'AR_BILL'
|
||||||
|
AND template_code = 'AR_CUSTOMER_BILL'
|
||||||
|
AND template_version = '1.0'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ar_bill_export (
|
CREATE TABLE IF NOT EXISTS ar_bill_export (
|
||||||
export_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出ID',
|
export_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '导出ID',
|
||||||
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
ar_bill_id BIGINT(20) NOT NULL COMMENT '应收账单ID',
|
||||||
file_name VARCHAR(255) NOT NULL COMMENT '下载展示名称',
|
file_name VARCHAR(255) NOT NULL COMMENT '下载展示名称',
|
||||||
file_type VARCHAR(20) NOT NULL COMMENT 'XLSX/PDF',
|
file_type VARCHAR(20) NOT NULL COMMENT 'XLSX/PDF',
|
||||||
|
template_id BIGINT(20) DEFAULT NULL COMMENT '提交导出任务时绑定的模板ID',
|
||||||
template_code VARCHAR(50) NOT NULL COMMENT '导出模板标识',
|
template_code VARCHAR(50) NOT NULL COMMENT '导出模板标识',
|
||||||
template_version VARCHAR(50) DEFAULT NULL COMMENT '模板版本',
|
template_version VARCHAR(50) DEFAULT NULL COMMENT '模板版本',
|
||||||
|
template_storage_type VARCHAR(20) DEFAULT NULL COMMENT '绑定模板存储类型',
|
||||||
|
template_storage_key VARCHAR(500) DEFAULT NULL COMMENT '绑定模板存储Key',
|
||||||
|
template_hash VARCHAR(128) DEFAULT NULL COMMENT '绑定模板SHA-256校验值',
|
||||||
storage_key VARCHAR(500) NOT NULL COMMENT '对象存储或本地文件Key',
|
storage_key VARCHAR(500) NOT NULL COMMENT '对象存储或本地文件Key',
|
||||||
file_size BIGINT(20) DEFAULT NULL COMMENT '文件字节数',
|
file_size BIGINT(20) DEFAULT NULL COMMENT '文件字节数',
|
||||||
file_hash VARCHAR(128) DEFAULT NULL COMMENT 'SHA-256校验值',
|
file_hash VARCHAR(128) DEFAULT NULL COMMENT 'SHA-256校验值',
|
||||||
@@ -115,6 +185,7 @@ CREATE TABLE IF NOT EXISTS ar_bill_export (
|
|||||||
remark VARCHAR(500) DEFAULT NULL COMMENT '失败原因或备注',
|
remark VARCHAR(500) DEFAULT NULL COMMENT '失败原因或备注',
|
||||||
PRIMARY KEY (export_id),
|
PRIMARY KEY (export_id),
|
||||||
KEY idx_ar_bill_export_bill (ar_bill_id, export_time),
|
KEY idx_ar_bill_export_bill (ar_bill_id, export_time),
|
||||||
|
KEY idx_ar_bill_export_template (template_id),
|
||||||
KEY idx_ar_bill_export_status (export_status),
|
KEY idx_ar_bill_export_status (export_status),
|
||||||
CONSTRAINT fk_ar_bill_export_bill FOREIGN KEY (ar_bill_id) REFERENCES ar_bill (ar_bill_id)
|
CONSTRAINT fk_ar_bill_export_bill FOREIGN KEY (ar_bill_id) REFERENCES ar_bill (ar_bill_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单导出文件元数据';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单导出文件元数据';
|
||||||
@@ -145,4 +216,144 @@ CREATE TABLE IF NOT EXISTS ar_bill_log (
|
|||||||
KEY idx_ar_bill_log_request (request_id)
|
KEY idx_ar_bill_log_request (request_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单操作日志';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应收账单操作日志';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 兼容已执行旧版本脚本的增量补齐逻辑
|
||||||
|
-- 本文件是应收账单模块唯一数据库脚本,新库和存量库均可重复执行。
|
||||||
|
-- ============================================================
|
||||||
|
SET @ar_db_name := DATABASE();
|
||||||
|
|
||||||
|
-- 1. 异步生成任务依赖字段
|
||||||
|
SET @ar_has_generation_status := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill'
|
||||||
|
AND COLUMN_NAME = 'generation_status'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_generation_status = 0,
|
||||||
|
'ALTER TABLE ar_bill ADD COLUMN generation_status VARCHAR(20) NOT NULL DEFAULT ''READY'' COMMENT ''生成状态:GENERATING=生成中,READY=可用,FAILED=生成失败'' AFTER status',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
-- 异步生成任务状态增加中文含义,便于数据库运维和排查。
|
||||||
|
-- 已存在 status 时只修改定义,不重复新增;兼容历史上已创建但字段不完整的任务表。
|
||||||
|
SET @ar_has_task_status := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_generate_task'
|
||||||
|
AND COLUMN_NAME = 'status'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_task_status = 0,
|
||||||
|
'ALTER TABLE ar_bill_generate_task ADD COLUMN status VARCHAR(30) NOT NULL COMMENT ''任务状态:WAITING=待处理,RUNNING=生成中,SUCCESS=生成成功,FAILED=生成失败,CANCEL_REQUESTED=取消中,CANCELED=已取消'' AFTER total_amount',
|
||||||
|
'ALTER TABLE ar_bill_generate_task MODIFY COLUMN status VARCHAR(30) NOT NULL COMMENT ''任务状态:WAITING=待处理,RUNNING=生成中,SUCCESS=生成成功,FAILED=生成失败,CANCEL_REQUESTED=取消中,CANCELED=已取消''');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
UPDATE ar_bill
|
||||||
|
SET generation_status = 'READY'
|
||||||
|
WHERE generation_status IS NULL;
|
||||||
|
|
||||||
|
-- 2. 为异步游标查询补充来源订单索引。
|
||||||
|
-- 来源表可能尚未由价格模块脚本创建,因此这里做存在性判断。
|
||||||
|
SET @ar_has_source_table := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'prc_order_bill'
|
||||||
|
);
|
||||||
|
SET @ar_has_source_cursor_index := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'prc_order_bill'
|
||||||
|
AND INDEX_NAME = 'idx_bill_ar_async_cursor'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_source_table = 1 AND @ar_has_source_cursor_index = 0,
|
||||||
|
'ALTER TABLE prc_order_bill ADD INDEX idx_bill_ar_async_cursor (customer_id, currency, ar_bill_status, create_time, bill_id)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
-- 3. 兼容已存在的模板表和导出记录表,补齐模板字段、注释及索引。
|
||||||
|
ALTER TABLE ar_export_template
|
||||||
|
MODIFY COLUMN storage_type VARCHAR(20) NOT NULL DEFAULT 'CLASSPATH'
|
||||||
|
COMMENT '存储类型:CLASSPATH=应用内置资源,FILE=服务器文件,OSS=对象存储,MINIO=MinIO对象存储',
|
||||||
|
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ENABLED'
|
||||||
|
COMMENT '状态:ENABLED=启用,DISABLED=停用';
|
||||||
|
|
||||||
|
SET @ar_has_template_id := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_export'
|
||||||
|
AND COLUMN_NAME = 'template_id'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_template_id = 0,
|
||||||
|
'ALTER TABLE ar_bill_export ADD COLUMN template_id BIGINT(20) DEFAULT NULL COMMENT ''提交导出任务时绑定的模板ID'' AFTER file_type',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
SET @ar_has_template_storage_type := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_export'
|
||||||
|
AND COLUMN_NAME = 'template_storage_type'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_template_storage_type = 0,
|
||||||
|
'ALTER TABLE ar_bill_export ADD COLUMN template_storage_type VARCHAR(20) DEFAULT NULL COMMENT ''绑定模板存储类型'' AFTER template_version',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
SET @ar_has_template_storage_key := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_export'
|
||||||
|
AND COLUMN_NAME = 'template_storage_key'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_template_storage_key = 0,
|
||||||
|
'ALTER TABLE ar_bill_export ADD COLUMN template_storage_key VARCHAR(500) DEFAULT NULL COMMENT ''绑定模板存储Key'' AFTER template_storage_type',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
SET @ar_has_template_hash := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_export'
|
||||||
|
AND COLUMN_NAME = 'template_hash'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_template_hash = 0,
|
||||||
|
'ALTER TABLE ar_bill_export ADD COLUMN template_hash VARCHAR(128) DEFAULT NULL COMMENT ''绑定模板SHA-256校验值'' AFTER template_storage_key',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
|
SET @ar_has_template_index := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @ar_db_name
|
||||||
|
AND TABLE_NAME = 'ar_bill_export'
|
||||||
|
AND INDEX_NAME = 'idx_ar_bill_export_template'
|
||||||
|
);
|
||||||
|
SET @ar_sql := IF(@ar_has_template_index = 0,
|
||||||
|
'ALTER TABLE ar_bill_export ADD INDEX idx_ar_bill_export_template (template_id)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE ar_stmt FROM @ar_sql;
|
||||||
|
EXECUTE ar_stmt;
|
||||||
|
DEALLOCATE PREPARE ar_stmt;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 应收账单接口统一放在这里;生成和导出均为异步任务,提交接口不等待后台处理完成。
|
||||||
export function listArBill(query) {
|
export function listArBill(query) {
|
||||||
return request({ url: '/ar/bill/list', method: 'get', params: query })
|
return request({ url: '/ar/bill/list', method: 'get', params: query })
|
||||||
}
|
}
|
||||||
@@ -8,12 +9,36 @@ export function getArBill(arBillId) {
|
|||||||
return request({ url: '/ar/bill/' + arBillId, method: 'get', timeout: 30000 })
|
return request({ url: '/ar/bill/' + arBillId, method: 'get', timeout: 30000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listArBillDetails(arBillId, query = {}) {
|
||||||
|
return request({ url: '/ar/bill/' + arBillId + '/details', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listArBillDetailFees(arBillId, detailId) {
|
||||||
|
return request({ url: '/ar/bill/' + arBillId + '/details/' + detailId + '/fees', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
export function getDefaultArBankAccount(currency = 'CNY') {
|
export function getDefaultArBankAccount(currency = 'CNY') {
|
||||||
return request({ url: '/ar/bill/default-bank-account', method: 'get', params: { currency } })
|
return request({ url: '/ar/bill/default-bank-account', method: 'get', params: { currency } })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateArBill(data) {
|
export function generateArBill(data) {
|
||||||
return request({ url: '/ar/bill/generate', method: 'post', data, timeout: 60000 })
|
return request({ url: '/ar/bill/generate', method: 'post', data, timeout: 30000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArBillGenerateTask(taskId) {
|
||||||
|
return request({ url: '/ar/bill/generate/task/' + taskId, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listActiveArBillGenerateTasks() {
|
||||||
|
return request({ url: '/ar/bill/generate/tasks/active', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelArBillGenerateTask(taskId) {
|
||||||
|
return request({ url: '/ar/bill/generate/task/' + taskId + '/cancel', method: 'post' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retryArBillGenerateTask(taskId) {
|
||||||
|
return request({ url: '/ar/bill/generate/task/' + taskId + '/retry', method: 'post' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewArBill(query) {
|
export function previewArBill(query) {
|
||||||
@@ -36,6 +61,10 @@ export function listArBillExports(arBillId) {
|
|||||||
return request({ url: '/ar/bill/' + arBillId + '/exports', method: 'get' })
|
return request({ url: '/ar/bill/' + arBillId + '/exports', method: 'get' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getArBillExport(exportId) {
|
||||||
|
return request({ url: '/ar/bill/export/' + exportId, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
export function downloadArBillExport(exportId) {
|
export function downloadArBillExport(exportId) {
|
||||||
return request({ url: '/ar/bill/export/' + exportId + '/download', method: 'get', responseType: 'blob' })
|
return request({ url: '/ar/bill/export/' + exportId + '/download', method: 'get', responseType: 'blob' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export const AR_BILL_STATUS_TYPES = Object.freeze({
|
|||||||
[AR_BILL_STATUS.VOID]: 'danger'
|
[AR_BILL_STATUS.VOID]: 'danger'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 账单生成时的订单选择模式,value 与后端枚举和数据库 code 保持一致。 */
|
||||||
|
export const AR_BILL_SELECTION_MODE = Object.freeze({
|
||||||
|
EXPLICIT: 'EXPLICIT', // 手动选择当前预览结果
|
||||||
|
ALL_MATCHED: 'ALL_MATCHED' // 全选查询结果,再排除指定订单
|
||||||
|
})
|
||||||
|
|
||||||
/** 日志状态变化的兼容展示。日志是通用表,除账单状态外还可能保留历史收款状态。 */
|
/** 日志状态变化的兼容展示。日志是通用表,除账单状态外还可能保留历史收款状态。 */
|
||||||
export const AR_LOG_STATUS_OPTIONS = Object.freeze([
|
export const AR_LOG_STATUS_OPTIONS = Object.freeze([
|
||||||
...AR_BILL_STATUS_OPTIONS,
|
...AR_BILL_STATUS_OPTIONS,
|
||||||
|
|||||||
@@ -17,17 +17,21 @@
|
|||||||
<el-descriptions-item label="收款账号">{{ mask(detail.bankAccountNo) }}</el-descriptions-item>
|
<el-descriptions-item label="收款账号">{{ mask(detail.bankAccountNo) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<div class="section-title">订单快照({{ detail.orderCount || 0 }})</div>
|
<!-- 账单明细独立分页;费用明细在展开行时再懒加载。 -->
|
||||||
<el-table :data="detail.details || []" border size="small">
|
<div class="section-title">账单明细({{ detail.orderCount || 0 }})</div>
|
||||||
|
<el-table v-loading="detailListLoading" :data="detailRows" row-key="detailId" border size="small" @expand-change="handleExpandChange">
|
||||||
<el-table-column type="expand">
|
<el-table-column type="expand">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-table :data="row.fees || []" border size="small" class="fee-table">
|
<div v-loading="row.feeLoading" class="fee-panel">
|
||||||
<el-table-column prop="feeType" label="费用类型" width="140" />
|
<el-empty v-if="row.feesLoaded && !row.fees?.length" description="暂无费用明细" :image-size="60" />
|
||||||
<el-table-column prop="feeName" label="费用名称" min-width="160" />
|
<el-table v-else :data="row.fees || []" border size="small" class="fee-table">
|
||||||
<el-table-column prop="amount" label="金额" width="120" align="right"><template #default="{ row: fee }">{{ money(fee.amount) }}</template></el-table-column>
|
<el-table-column prop="feeType" label="费用类型" width="140" />
|
||||||
<el-table-column prop="currency" label="币种" width="80" />
|
<el-table-column prop="feeName" label="费用名称" min-width="160" />
|
||||||
<el-table-column prop="remark" label="说明" show-overflow-tooltip />
|
<el-table-column prop="amount" label="金额" width="120" align="right"><template #default="{ row: fee }">{{ money(fee.amount) }}</template></el-table-column>
|
||||||
</el-table>
|
<el-table-column prop="currency" label="币种" width="80" />
|
||||||
|
<el-table-column prop="remark" label="说明" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="detailNo" label="#" width="55" align="center" />
|
<el-table-column prop="detailNo" label="#" width="55" align="center" />
|
||||||
@@ -38,12 +42,24 @@
|
|||||||
<el-table-column label="重量/尺寸" min-width="170"><template #default="{ row }">{{ row.chargeWeight || '-' }} kg / {{ dimensions(row) }}</template></el-table-column>
|
<el-table-column label="重量/尺寸" min-width="170"><template #default="{ row }">{{ row.chargeWeight || '-' }} kg / {{ dimensions(row) }}</template></el-table-column>
|
||||||
<el-table-column prop="totalAmount" label="订单金额" width="120" align="right"><template #default="{ row }">{{ money(row.totalAmount) }}</template></el-table-column>
|
<el-table-column prop="totalAmount" label="订单金额" width="120" align="right"><template #default="{ row }">{{ money(row.totalAmount) }}</template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="detailTotal > 0"
|
||||||
|
class="detail-pagination"
|
||||||
|
v-model:current-page="detailPageNum"
|
||||||
|
v-model:page-size="detailPageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="detailTotal"
|
||||||
|
@current-change="loadDetails"
|
||||||
|
@size-change="handleDetailSizeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="section-title export-title"><span>导出历史</span><el-button size="small" type="primary" plain icon="Download" @click="$emit('export')">生成新文件</el-button></div>
|
<div class="section-title export-title"><span>导出历史</span><el-button size="small" type="primary" plain icon="Download" :loading="exporting" :disabled="exporting" @click="$emit('export')">{{ exporting ? '生成中' : '生成新文件' }}</el-button></div>
|
||||||
<el-table :data="detail.exports || []" border size="small">
|
<el-table :data="detail.exports || []" border size="small">
|
||||||
<el-table-column prop="fileName" label="文件名" min-width="250" show-overflow-tooltip />
|
<el-table-column prop="fileName" label="文件名" min-width="250" show-overflow-tooltip />
|
||||||
<el-table-column prop="templateVersion" label="模板版本" width="100" />
|
<el-table-column prop="templateVersion" label="模板版本" width="100" />
|
||||||
<el-table-column prop="exportStatus" label="状态" width="100"><template #default="{ row }"><el-tag :type="exportStatusType(row.exportStatus)">{{ exportStatusText(row.exportStatus) }}</el-tag></template></el-table-column>
|
<el-table-column prop="exportStatus" label="状态" width="100"><template #default="{ row }"><el-tag :type="exportStatusType(row.exportStatus)">{{ exportStatusText(row.exportStatus) }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column prop="remark" label="失败原因" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column prop="exportBy" label="导出人" width="100" />
|
<el-table-column prop="exportBy" label="导出人" width="100" />
|
||||||
<el-table-column prop="exportTime" label="导出时间" width="170" />
|
<el-table-column prop="exportTime" label="导出时间" width="170" />
|
||||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button v-if="row.exportStatus === AR_EXPORT_STATUS.SUCCESS" link type="primary" @click="$emit('download', row)">下载</el-button></template></el-table-column>
|
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button v-if="row.exportStatus === AR_EXPORT_STATUS.SUCCESS" link type="primary" @click="$emit('download', row)">下载</el-button></template></el-table-column>
|
||||||
@@ -54,10 +70,64 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { listArBillDetails, listArBillDetailFees } from '@/api/ar/bill'
|
||||||
import { AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES, AR_EXPORT_STATUS, AR_EXPORT_STATUS_OPTIONS, AR_EXPORT_STATUS_TYPES, arEnumLabel } from '@/constants/ar/enums'
|
import { AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES, AR_EXPORT_STATUS, AR_EXPORT_STATUS_OPTIONS, AR_EXPORT_STATUS_TYPES, arEnumLabel } from '@/constants/ar/enums'
|
||||||
|
|
||||||
defineProps({ modelValue: Boolean, detail: { type: Object, default: null }, loading: Boolean })
|
const props = defineProps({ modelValue: Boolean, detail: { type: Object, default: null }, loading: Boolean })
|
||||||
defineEmits(['update:modelValue', 'download', 'export'])
|
defineEmits(['update:modelValue', 'download', 'export'])
|
||||||
|
const detailRows = ref([])
|
||||||
|
const detailTotal = ref(0)
|
||||||
|
const detailPageNum = ref(1)
|
||||||
|
// 详情默认每页 10 条,避免几十万条账单明细一次性进入浏览器内存。
|
||||||
|
const detailPageSize = ref(10)
|
||||||
|
const detailListLoading = ref(false)
|
||||||
|
let detailRequestSerial = 0
|
||||||
|
const exporting = computed(() => props.detail?.exports?.some(item => item.exportStatus === AR_EXPORT_STATUS.PROCESSING))
|
||||||
|
|
||||||
|
// 切换账单时重置分页和请求序号,旧账单的慢响应不能覆盖新账单。
|
||||||
|
watch(() => props.detail?.arBillId, billId => {
|
||||||
|
detailRequestSerial += 1
|
||||||
|
detailPageNum.value = 1
|
||||||
|
detailTotal.value = 0
|
||||||
|
detailRows.value = []
|
||||||
|
if (billId) loadDetails()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 账单明细接口只返回当前页;分页变化时复用同一账单 ID 重新查询。
|
||||||
|
function loadDetails() {
|
||||||
|
const billId = props.detail?.arBillId
|
||||||
|
if (!billId) return
|
||||||
|
const serial = ++detailRequestSerial
|
||||||
|
detailListLoading.value = true
|
||||||
|
listArBillDetails(billId, { pageNum: detailPageNum.value, pageSize: detailPageSize.value }).then(res => {
|
||||||
|
if (serial !== detailRequestSerial) return
|
||||||
|
detailRows.value = (res.rows || []).map(row => ({ ...row, fees: [], feesLoaded: false, feeLoading: false }))
|
||||||
|
detailTotal.value = Number(res.total) || 0
|
||||||
|
}).finally(() => {
|
||||||
|
if (serial === detailRequestSerial) detailListLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDetailSizeChange(size) {
|
||||||
|
detailPageSize.value = size
|
||||||
|
detailPageNum.value = 1
|
||||||
|
loadDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 费用明细按行懒加载,并用标记避免同一行重复请求。
|
||||||
|
function handleExpandChange(row, expandedRows) {
|
||||||
|
if (!expandedRows.includes(row) || row.feesLoaded || row.feeLoading) return
|
||||||
|
const billId = props.detail?.arBillId
|
||||||
|
if (!billId || !row.detailId) return
|
||||||
|
row.feeLoading = true
|
||||||
|
listArBillDetailFees(billId, row.detailId).then(res => {
|
||||||
|
row.fees = res.data || []
|
||||||
|
row.feesLoaded = true
|
||||||
|
}).finally(() => {
|
||||||
|
row.feeLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function date(value) { return value ? String(value).slice(0, 10) : '-' }
|
function date(value) { return value ? String(value).slice(0, 10) : '-' }
|
||||||
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
|
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
|
||||||
function statusText(value) { return arEnumLabel(AR_BILL_STATUS_OPTIONS, value) }
|
function statusText(value) { return arEnumLabel(AR_BILL_STATUS_OPTIONS, value) }
|
||||||
@@ -75,5 +145,7 @@ function mask(value) { if (!value) return '-'; return value.length <= 4 ? '****'
|
|||||||
.section-title::before { content: ''; width: 3px; height: 15px; border-radius: 2px; background: var(--el-color-primary); }
|
.section-title::before { content: ''; width: 3px; height: 15px; border-radius: 2px; background: var(--el-color-primary); }
|
||||||
.export-title { justify-content: space-between; }
|
.export-title { justify-content: space-between; }
|
||||||
.export-title::before { margin-right: auto; }
|
.export-title::before { margin-right: auto; }
|
||||||
|
.detail-pagination { justify-content: flex-end; margin: 12px 0 4px; }
|
||||||
|
.fee-panel { min-height: 54px; padding: 4px 12px 8px 42px; background: #fafafa; }
|
||||||
.fee-table { background: #fafafa; }
|
.fee-table { background: #fafafa; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请选择客户" @change="handleCustomerChange" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请选择客户" @change="handleCustomerChange" /></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
|
||||||
|
<el-col :span="8"><el-form-item label="订单/运单号"><el-input v-model="generateForm.orderOrTrackingNo" placeholder="请输入订单号或运单号" clearable /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="payee-card" v-loading="bankAccountLoading">
|
<div class="payee-card" v-loading="bankAccountLoading">
|
||||||
@@ -18,14 +19,47 @@
|
|||||||
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
|
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-toolbar">
|
<div class="preview-toolbar">
|
||||||
<span v-if="previewSearched" class="preview-summary">可选 {{ previewRows.length }} 条订单,已选 <b>{{ selectedRows.length }}</b> 条,应收合计 <b>{{ money(selectedAmount) }}</b></span>
|
<span v-if="previewSearched" class="preview-summary">可选 {{ previewTotal }} 条订单,已选 <b>{{ displaySelectedCount }}</b> 条,应收合计 <b>{{ money(displaySelectedAmount) }}</b></span>
|
||||||
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</span>
|
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</span>
|
||||||
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
|
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="previewSearched" class="preview-selection-toolbar">
|
||||||
|
<el-tooltip content="全选全部查询结果" placement="top">
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="allSelected"
|
||||||
|
:disabled="!previewTotal"
|
||||||
|
aria-label="全选全部查询结果"
|
||||||
|
@change="handleSelectAllChange"
|
||||||
|
>
|
||||||
|
<el-icon><Select /></el-icon>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="切换手动选择" placement="top">
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="selectionMode === AR_BILL_SELECTION_MODE.EXPLICIT"
|
||||||
|
:disabled="!previewTotal || selectionMode === AR_BILL_SELECTION_MODE.EXPLICIT"
|
||||||
|
aria-label="切换手动选择"
|
||||||
|
@change="handleManualModeChange"
|
||||||
|
>
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-badge v-if="allSelected" :value="excludedRows.length" :hidden="!excludedRows.length" class="exclude-order-badge">
|
||||||
|
<el-tooltip content="选择排除订单" placement="top">
|
||||||
|
<el-button circle text :icon="Search" aria-label="选择排除订单" @click="openExcludeDialog" />
|
||||||
|
</el-tooltip>
|
||||||
|
</el-badge>
|
||||||
|
</div>
|
||||||
|
<div v-if="allSelected && excludedRows.length" class="excluded-order-preview">
|
||||||
|
<span class="excluded-order-title">排除订单:</span>
|
||||||
|
<el-tag v-for="row in excludedRows" :key="row.billId" closable size="small" @close="removeExcludedRow(row.billId)">
|
||||||
|
{{ row.orderNo || row.trackingNo || row.billId }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
<el-table ref="previewTableRef" v-loading="previewLoading" :data="previewRows" row-key="billId" border stripe max-height="400" empty-text="暂无符合条件的计费订单" @selection-change="handleSelectionChange">
|
<el-table ref="previewTableRef" v-loading="previewLoading" :data="previewRows" row-key="billId" border stripe max-height="400" empty-text="暂无符合条件的计费订单" @selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="50" fixed="left" />
|
<el-table-column type="selection" width="50" fixed="left" :selectable="isMainRowSelectable" />
|
||||||
<el-table-column prop="orderNo" label="订单号" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="orderNo" label="订单号" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="trackingNo" label="跟踪号" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="trackingNo" label="运单号" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="customerName" label="客户" min-width="130" show-overflow-tooltip />
|
<el-table-column prop="customerName" label="客户" min-width="130" show-overflow-tooltip />
|
||||||
<el-table-column prop="bizDate" label="业务日期" width="105" :formatter="formatDateColumn" />
|
<el-table-column prop="bizDate" label="业务日期" width="105" :formatter="formatDateColumn" />
|
||||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||||
@@ -35,20 +69,59 @@
|
|||||||
<el-table-column prop="incomeTotal" label="应收金额" width="110" align="right" :formatter="formatNumberColumn" />
|
<el-table-column prop="incomeTotal" label="应收金额" width="110" align="right" :formatter="formatNumberColumn" />
|
||||||
<el-table-column prop="currency" label="币种" width="75" />
|
<el-table-column prop="currency" label="币种" width="75" />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="previewTotal > 0"
|
||||||
|
class="preview-pagination"
|
||||||
|
:current-page="previewPageNum"
|
||||||
|
:page-size="previewPageSize"
|
||||||
|
layout="total, prev, pager, next, jumper"
|
||||||
|
:total="previewTotal"
|
||||||
|
@current-change="handlePreviewPageChange"
|
||||||
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="generate-footer-summary">已选 {{ selectedRows.length }} 条订单 · {{ money(selectedAmount) }}</span>
|
<span class="generate-footer-summary">已选 {{ displaySelectedCount }} 条订单 · {{ money(displaySelectedAmount) }}</span>
|
||||||
<el-button type="primary" :loading="generateLoading" :disabled="!bankAccount || !previewSearched || !selectedRows.length" @click="submitGenerate">生成账单</el-button>
|
<el-button type="primary" :loading="generateLoading" :disabled="!bankAccount || !previewSearched || !displaySelectedCount" @click="submitGenerate">生成账单</el-button>
|
||||||
<el-button @click="dialogOpen = false">取消</el-button>
|
<el-button @click="dialogOpen = false">取消</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose">
|
||||||
|
<div class="exclude-toolbar">
|
||||||
|
<el-input v-model="excludeKeyword" clearable placeholder="请输入订单号或运单号" class="exclude-search-input" />
|
||||||
|
<el-button type="primary" :loading="excludeLoading" @click="queryExcludeRows">查询</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table ref="excludeTableRef" v-loading="excludeLoading" :data="excludeRowsPage" row-key="billId" border stripe max-height="420" empty-text="暂无符合条件的订单" @selection-change="handleExcludeSelectionChange">
|
||||||
|
<el-table-column type="selection" width="50" fixed="left" />
|
||||||
|
<el-table-column prop="orderNo" label="订单号" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="trackingNo" label="运单号" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="customerName" label="客户" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="bizDate" label="业务日期" width="120" :formatter="formatDateColumn" />
|
||||||
|
<el-table-column prop="incomeTotal" label="应收金额" width="130" align="right" :formatter="formatNumberColumn" />
|
||||||
|
<el-table-column prop="currency" label="币种" width="80" />
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="excludeTotal > 0"
|
||||||
|
class="exclude-pagination"
|
||||||
|
:current-page="excludePageNum"
|
||||||
|
:page-size="excludePageSize"
|
||||||
|
layout="total, prev, pager, next, jumper"
|
||||||
|
:total="excludeTotal"
|
||||||
|
@current-change="handleExcludePageChange"
|
||||||
|
/>
|
||||||
|
<template #footer>
|
||||||
|
<span class="generate-footer-summary">已选择 {{ excludeDraftRows.length }} 条排除订单</span>
|
||||||
|
<el-button @click="cancelExcludeSelection">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmExcludeSelection">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="billNameOpen" title="填写账单名称" width="520px" append-to-body destroy-on-close>
|
<el-dialog v-model="billNameOpen" title="填写账单名称" width="520px" append-to-body destroy-on-close>
|
||||||
<el-form ref="billNameFormRef" :model="billNameForm" :rules="billNameRules" label-width="90px">
|
<el-form ref="billNameFormRef" :model="billNameForm" :rules="billNameRules" label-width="90px">
|
||||||
<el-form-item label="账单名称" prop="billName">
|
<el-form-item label="账单名称" prop="billName">
|
||||||
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
|
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="bill-name-summary">已选择 {{ selectedRows.length }} 条订单,应收合计 {{ money(selectedAmount) }}</div>
|
<div class="bill-name-summary">已选择 {{ displaySelectedCount }} 条订单,应收合计 {{ money(displaySelectedAmount) }}</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="billNameOpen = false">取消</el-button>
|
<el-button @click="billNameOpen = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
|
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
|
||||||
@@ -59,6 +132,8 @@
|
|||||||
|
|
||||||
<script setup name="GenerateBillDialog">
|
<script setup name="GenerateBillDialog">
|
||||||
import { getDefaultArBankAccount, previewArBill, generateArBill } from '@/api/ar/bill'
|
import { getDefaultArBankAccount, previewArBill, generateArBill } from '@/api/ar/bill'
|
||||||
|
import { Refresh, Search, Select } from '@element-plus/icons-vue'
|
||||||
|
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -78,14 +153,39 @@ const generateLoading = ref(false)
|
|||||||
const previewLoading = ref(false)
|
const previewLoading = ref(false)
|
||||||
const previewSearched = ref(false)
|
const previewSearched = ref(false)
|
||||||
const previewRows = ref([])
|
const previewRows = ref([])
|
||||||
|
const previewTotal = ref(0)
|
||||||
|
const previewTotalAmount = ref(0)
|
||||||
|
const previewPageNum = ref(1)
|
||||||
|
// 主预览列表固定每页 5 条;Map 负责保存跨页勾选结果。
|
||||||
|
const previewPageSize = 5
|
||||||
const previewTableRef = ref()
|
const previewTableRef = ref()
|
||||||
const selectedRows = ref([])
|
const selectedRows = ref([])
|
||||||
|
const selectedRowMap = new Map()
|
||||||
|
// 选择模式只保存编码,提交时由后端枚举再次校验。
|
||||||
|
const selectionMode = ref(AR_BILL_SELECTION_MODE.EXPLICIT)
|
||||||
|
const excludedRows = ref([])
|
||||||
|
const excludedRowMap = new Map()
|
||||||
|
const excludeDialogOpen = ref(false)
|
||||||
|
const excludeLoading = ref(false)
|
||||||
|
const excludeKeyword = ref('')
|
||||||
|
const excludeRowsPage = ref([])
|
||||||
|
const excludeTotal = ref(0)
|
||||||
|
const excludePageNum = ref(1)
|
||||||
|
// 全选后的排除弹窗也按 5 条分页,查询条件固定继承主弹窗条件。
|
||||||
|
const excludePageSize = 5
|
||||||
|
const excludeTableRef = ref()
|
||||||
|
const excludeDraftRows = ref([])
|
||||||
|
let excludeDraftMap = new Map()
|
||||||
|
let excludeDialogConfirmed = false
|
||||||
|
let syncingExcludeSelection = false
|
||||||
|
const previewQuery = ref(null)
|
||||||
|
let syncingPageSelection = false
|
||||||
const bankAccountLoading = ref(false)
|
const bankAccountLoading = ref(false)
|
||||||
const bankAccount = ref(null)
|
const bankAccount = ref(null)
|
||||||
const generateRequestId = ref('')
|
const generateRequestId = ref('')
|
||||||
const generateFormRef = ref()
|
const generateFormRef = ref()
|
||||||
const billNameFormRef = ref()
|
const billNameFormRef = ref()
|
||||||
const generateForm = reactive({ customerId: '', customerCode: '', cutoffDate: '', currency: 'CNY' })
|
const generateForm = reactive({ customerId: '', customerCode: '', customerName: '', cutoffDate: '', currency: 'CNY', orderOrTrackingNo: '' })
|
||||||
const billNameForm = reactive({ billName: '' })
|
const billNameForm = reactive({ billName: '' })
|
||||||
const generateRules = {
|
const generateRules = {
|
||||||
customerId: [{ required: true, message: '客户不能为空', trigger: 'change' }],
|
customerId: [{ required: true, message: '客户不能为空', trigger: 'change' }],
|
||||||
@@ -94,6 +194,12 @@ const generateRules = {
|
|||||||
}
|
}
|
||||||
const billNameRules = { billName: [{ required: true, message: '请输入账单名称', trigger: 'blur' }] }
|
const billNameRules = { billName: [{ required: true, message: '请输入账单名称', trigger: 'blur' }] }
|
||||||
const selectedAmount = computed(() => selectedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
|
const selectedAmount = computed(() => selectedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
|
||||||
|
const excludedAmount = computed(() => excludedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
|
||||||
|
const allSelected = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED)
|
||||||
|
const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||||
|
? Math.max(previewTotal.value - excludedRows.value.length, 0) : selectedRows.value.length)
|
||||||
|
const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||||
|
? Math.max(Number(previewTotalAmount.value || 0) - excludedAmount.value, 0) : selectedAmount.value)
|
||||||
|
|
||||||
watch(() => props.modelValue, visible => {
|
watch(() => props.modelValue, visible => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -101,8 +207,9 @@ watch(() => props.modelValue, visible => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 每次打开生成弹窗都重新建立一次查询快照,避免复用上一次的选中订单。
|
||||||
function openGenerate() {
|
function openGenerate() {
|
||||||
Object.assign(generateForm, { customerId: '', customerCode: '', cutoffDate: '', currency: 'CNY' })
|
Object.assign(generateForm, { customerId: '', customerCode: '', customerName: '', cutoffDate: '', currency: 'CNY', orderOrTrackingNo: '' })
|
||||||
Object.assign(billNameForm, { billName: '' })
|
Object.assign(billNameForm, { billName: '' })
|
||||||
billNameOpen.value = false
|
billNameOpen.value = false
|
||||||
resetPreview()
|
resetPreview()
|
||||||
@@ -113,6 +220,7 @@ function openGenerate() {
|
|||||||
|
|
||||||
function handleCustomerChange(_, item) {
|
function handleCustomerChange(_, item) {
|
||||||
generateForm.customerCode = item?.customerCode || ''
|
generateForm.customerCode = item?.customerCode || ''
|
||||||
|
generateForm.customerName = item?.customerName || ''
|
||||||
resetPreview()
|
resetPreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +239,21 @@ function loadDefaultBankAccount() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 条件变化后清理预览和跨页选中状态;用户点击查询时重新加载第一页。
|
||||||
function resetPreview() {
|
function resetPreview() {
|
||||||
previewRows.value = []
|
previewRows.value = []
|
||||||
|
previewTotal.value = 0
|
||||||
|
previewTotalAmount.value = 0
|
||||||
|
previewPageNum.value = 1
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
|
selectedRowMap.clear()
|
||||||
|
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
|
||||||
|
excludedRows.value = []
|
||||||
|
excludedRowMap.clear()
|
||||||
|
excludeDraftRows.value = []
|
||||||
|
excludeDraftMap = new Map()
|
||||||
|
excludeDialogOpen.value = false
|
||||||
|
previewQuery.value = null
|
||||||
previewSearched.value = false
|
previewSearched.value = false
|
||||||
generateRequestId.value = ''
|
generateRequestId.value = ''
|
||||||
nextTick(() => previewTableRef.value?.clearSelection())
|
nextTick(() => previewTableRef.value?.clearSelection())
|
||||||
@@ -143,41 +263,225 @@ function buildGenerateQuery() {
|
|||||||
return {
|
return {
|
||||||
customerId: Number(generateForm.customerId),
|
customerId: Number(generateForm.customerId),
|
||||||
customerCode: generateForm.customerCode?.trim() || undefined,
|
customerCode: generateForm.customerCode?.trim() || undefined,
|
||||||
|
customerName: generateForm.customerName?.trim() || undefined,
|
||||||
billPeriodEnd: generateForm.cutoffDate,
|
billPeriodEnd: generateForm.cutoffDate,
|
||||||
currency: generateForm.currency || undefined
|
currency: generateForm.currency || undefined,
|
||||||
|
orderOrTrackingNo: generateForm.orderOrTrackingNo?.trim() || undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 任务请求携带完整筛选快照;全选模式只提交排除 ID,后台按条件游标扫描其余订单。
|
||||||
function buildGenerateRequest(requestId) {
|
function buildGenerateRequest(requestId) {
|
||||||
return {
|
return {
|
||||||
...buildGenerateQuery(),
|
...(previewQuery.value || buildGenerateQuery()),
|
||||||
billName: billNameForm.billName.trim(),
|
billName: billNameForm.billName.trim(),
|
||||||
sourceBillIds: selectedRows.value.map(row => row.billId),
|
selectionMode: selectionMode.value,
|
||||||
|
sourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? [] : selectedRows.value.map(row => row.billId),
|
||||||
|
excludedSourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? Array.from(excludedRowMap.keys()) : [],
|
||||||
requestId
|
requestId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPreviewQuery(pageNum = previewPageNum.value, pageSize = previewPageSize) {
|
||||||
|
return { ...(previewQuery.value || buildGenerateQuery()), pageNum, pageSize }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询按钮是预览入口,后端返回当前页、总数和全量金额合计。
|
||||||
function queryGeneratePreview() {
|
function queryGeneratePreview() {
|
||||||
generateFormRef.value.validate(valid => {
|
generateFormRef.value.validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
|
const query = buildGenerateQuery()
|
||||||
resetPreview()
|
resetPreview()
|
||||||
previewLoading.value = true
|
previewQuery.value = query
|
||||||
previewArBill(buildGenerateQuery()).then(res => {
|
fetchPreview()
|
||||||
previewRows.value = res.rows || []
|
|
||||||
previewSearched.value = true
|
|
||||||
}).finally(() => {
|
|
||||||
previewLoading.value = false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchPreview() {
|
||||||
|
previewLoading.value = true
|
||||||
|
previewArBill(buildPreviewQuery()).then(res => {
|
||||||
|
syncingPageSelection = true
|
||||||
|
previewRows.value = res.rows || []
|
||||||
|
previewTotal.value = Number(res.total) || 0
|
||||||
|
previewTotalAmount.value = Number(res.totalAmount) || 0
|
||||||
|
previewSearched.value = true
|
||||||
|
nextTick(syncPageSelection)
|
||||||
|
}).finally(() => {
|
||||||
|
previewLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePreviewPageChange(page) {
|
||||||
|
previewPageNum.value = page
|
||||||
|
fetchPreview()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化只更新当前页 ID,selectedRowMap 因此可以跨页保留手动选择。
|
||||||
function handleSelectionChange(rows) {
|
function handleSelectionChange(rows) {
|
||||||
selectedRows.value = rows
|
if (syncingPageSelection || selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED) return
|
||||||
|
const currentPageIds = new Set(previewRows.value.map(row => row.billId))
|
||||||
|
currentPageIds.forEach(billId => selectedRowMap.delete(billId))
|
||||||
|
rows.forEach(row => selectedRowMap.set(row.billId, row))
|
||||||
|
selectedRows.value = Array.from(selectedRowMap.values())
|
||||||
if (!generateLoading.value) generateRequestId.value = ''
|
if (!generateLoading.value) generateRequestId.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全选后主表复选框全部禁用,用户通过排除弹窗维护“不生成”的订单。
|
||||||
|
function handleSelectAllChange(checked) {
|
||||||
|
if (!checked) {
|
||||||
|
switchToManualMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectionMode.value = AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||||
|
excludedRows.value = []
|
||||||
|
excludedRowMap.clear()
|
||||||
|
selectedRowMap.clear()
|
||||||
|
selectedRows.value = []
|
||||||
|
if (!generateLoading.value) generateRequestId.value = ''
|
||||||
|
nextTick(syncPageSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleManualModeChange(checked) {
|
||||||
|
if (checked) switchToManualMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从全选切回手动选择时清除排除集合,恢复主表跨页勾选流程。
|
||||||
|
function switchToManualMode() {
|
||||||
|
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
|
||||||
|
excludedRows.value = []
|
||||||
|
excludedRowMap.clear()
|
||||||
|
selectedRowMap.clear()
|
||||||
|
selectedRows.value = []
|
||||||
|
if (!generateLoading.value) generateRequestId.value = ''
|
||||||
|
nextTick(syncPageSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMainRowSelectable() {
|
||||||
|
return selectionMode.value !== AR_BILL_SELECTION_MODE.ALL_MATCHED
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExcludedRows() {
|
||||||
|
excludedRows.value = Array.from(excludedRowMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExcludeDraftRows() {
|
||||||
|
excludeDraftRows.value = Array.from(excludeDraftMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除弹窗继承客户、币种、截止日和主关键字,只额外允许订单号/运单号 OR 查询。
|
||||||
|
function openExcludeDialog() {
|
||||||
|
if (!allSelected.value) return
|
||||||
|
excludeDialogConfirmed = false
|
||||||
|
excludeDraftMap = new Map(excludedRowMap)
|
||||||
|
syncExcludeDraftRows()
|
||||||
|
excludeKeyword.value = ''
|
||||||
|
excludePageNum.value = 1
|
||||||
|
excludeRowsPage.value = []
|
||||||
|
excludeTotal.value = 0
|
||||||
|
excludeDialogOpen.value = true
|
||||||
|
fetchExcludeRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExcludeQuery() {
|
||||||
|
return {
|
||||||
|
...(previewQuery.value || {}),
|
||||||
|
excludeOrderOrTrackingNo: excludeKeyword.value?.trim() || undefined,
|
||||||
|
pageNum: excludePageNum.value,
|
||||||
|
pageSize: excludePageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryExcludeRows() {
|
||||||
|
excludePageNum.value = 1
|
||||||
|
fetchExcludeRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchExcludeRows() {
|
||||||
|
if (!previewQuery.value) return
|
||||||
|
excludeLoading.value = true
|
||||||
|
previewArBill(buildExcludeQuery()).then(res => {
|
||||||
|
syncingExcludeSelection = true
|
||||||
|
excludeRowsPage.value = res.rows || []
|
||||||
|
excludeTotal.value = Number(res.total) || 0
|
||||||
|
nextTick(syncExcludePageSelection)
|
||||||
|
}).finally(() => {
|
||||||
|
excludeLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExcludePageChange(page) {
|
||||||
|
excludePageNum.value = page
|
||||||
|
fetchExcludeRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除弹窗使用独立 Map 保存跨页选择,关闭或翻页不会丢失已勾选订单。
|
||||||
|
function handleExcludeSelectionChange(rows) {
|
||||||
|
if (syncingExcludeSelection) return
|
||||||
|
const currentPageIds = new Set(excludeRowsPage.value.map(row => row.billId))
|
||||||
|
currentPageIds.forEach(billId => excludeDraftMap.delete(billId))
|
||||||
|
rows.forEach(row => excludeDraftMap.set(row.billId, row))
|
||||||
|
syncExcludeDraftRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExcludePageSelection() {
|
||||||
|
if (!excludeTableRef.value) {
|
||||||
|
syncingExcludeSelection = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncingExcludeSelection = true
|
||||||
|
excludeTableRef.value.clearSelection()
|
||||||
|
excludeRowsPage.value.forEach(row => {
|
||||||
|
if (excludeDraftMap.has(row.billId)) excludeTableRef.value.toggleRowSelection(row, true)
|
||||||
|
})
|
||||||
|
syncingExcludeSelection = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmExcludeSelection() {
|
||||||
|
excludedRowMap.clear()
|
||||||
|
excludeDraftMap.forEach((row, billId) => excludedRowMap.set(billId, row))
|
||||||
|
syncExcludedRows()
|
||||||
|
if (!generateLoading.value) generateRequestId.value = ''
|
||||||
|
excludeDialogConfirmed = true
|
||||||
|
excludeDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeExcludedRow(billId) {
|
||||||
|
excludedRowMap.delete(billId)
|
||||||
|
excludeDraftMap.delete(billId)
|
||||||
|
syncExcludeDraftRows()
|
||||||
|
syncExcludedRows()
|
||||||
|
if (!generateLoading.value) generateRequestId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelExcludeSelection() {
|
||||||
|
excludeDialogConfirmed = false
|
||||||
|
excludeDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExcludeDialogClose() {
|
||||||
|
if (!excludeDialogConfirmed) {
|
||||||
|
excludeDraftMap = new Map(excludedRowMap)
|
||||||
|
syncExcludeDraftRows()
|
||||||
|
}
|
||||||
|
excludeDialogConfirmed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页后把 Map 中的选择同步回 Element Plus 当前页复选框。
|
||||||
|
function syncPageSelection() {
|
||||||
|
if (!previewTableRef.value) {
|
||||||
|
syncingPageSelection = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncingPageSelection = true
|
||||||
|
previewTableRef.value.clearSelection()
|
||||||
|
previewRows.value.forEach(row => {
|
||||||
|
if (selectedRowMap.has(row.billId)) previewTableRef.value.toggleRowSelection(row, true)
|
||||||
|
})
|
||||||
|
syncingPageSelection = false
|
||||||
|
}
|
||||||
|
|
||||||
function submitGenerate() {
|
function submitGenerate() {
|
||||||
if (!previewSearched.value || !selectedRows.value.length) {
|
if (!previewSearched.value || !displaySelectedCount.value) {
|
||||||
proxy.$modal.msgError('请至少选择一条计费订单')
|
proxy.$modal.msgError('请至少选择一条计费订单')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -186,13 +490,14 @@ function submitGenerate() {
|
|||||||
nextTick(() => billNameFormRef.value?.clearValidate())
|
nextTick(() => billNameFormRef.value?.clearValidate())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最终只提交异步任务,不在浏览器等待几十万条订单明细生成完成。
|
||||||
function confirmGenerate() {
|
function confirmGenerate() {
|
||||||
billNameFormRef.value.validate(valid => {
|
billNameFormRef.value.validate(valid => {
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
if (!generateRequestId.value) generateRequestId.value = createRequestId()
|
if (!generateRequestId.value) generateRequestId.value = createRequestId()
|
||||||
generateLoading.value = true
|
generateLoading.value = true
|
||||||
generateArBill(buildGenerateRequest(generateRequestId.value)).then(res => {
|
generateArBill(buildGenerateRequest(generateRequestId.value)).then(res => {
|
||||||
proxy.$modal.msgSuccess('账单生成成功')
|
proxy.$modal.msgSuccess('账单生成任务已提交,后台正在生成')
|
||||||
billNameOpen.value = false
|
billNameOpen.value = false
|
||||||
dialogOpen.value = false
|
dialogOpen.value = false
|
||||||
emit('success', res.data)
|
emit('success', res.data)
|
||||||
@@ -219,6 +524,16 @@ function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${D
|
|||||||
.preview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 18px 0 10px; padding: 10px 14px; border-radius: 6px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); }
|
.preview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 18px 0 10px; padding: 10px 14px; border-radius: 6px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); }
|
||||||
.preview-summary { flex: 1; font-size: 14px; }
|
.preview-summary { flex: 1; font-size: 14px; }
|
||||||
.preview-summary b { color: var(--el-color-primary); }
|
.preview-summary b { color: var(--el-color-primary); }
|
||||||
|
.preview-selection-toolbar { display: flex; align-items: center; gap: 6px; min-height: 32px; padding-left: 8px; }
|
||||||
|
.preview-selection-toolbar :deep(.el-checkbox) { margin-right: 6px; }
|
||||||
|
.preview-selection-toolbar :deep(.el-checkbox__label) { display: inline-flex; align-items: center; padding-left: 4px; font-size: 16px; }
|
||||||
|
.exclude-order-badge { margin-left: 4px; }
|
||||||
|
.excluded-order-preview { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; max-height: 64px; margin: 0 0 8px; padding: 4px 8px; overflow-y: auto; border-radius: 4px; background: var(--el-fill-color-light); }
|
||||||
|
.excluded-order-title { color: var(--el-text-color-secondary); font-size: 13px; }
|
||||||
|
.exclude-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
|
.exclude-search-input { width: 360px; }
|
||||||
|
.exclude-pagination { justify-content: flex-end; margin: 12px 0 4px; }
|
||||||
|
.preview-pagination { justify-content: flex-end; margin: 12px 0 4px; }
|
||||||
.bill-name-summary { margin: -4px 0 8px 90px; color: var(--el-text-color-secondary); font-size: 13px; }
|
.bill-name-summary { margin: -4px 0 8px 90px; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||||
.generate-footer-summary { margin-right: auto; color: var(--el-text-color-secondary); font-size: 13px; }
|
.generate-footer-summary { margin-right: auto; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container ar-bill-page">
|
<div class="app-container ar-bill-page">
|
||||||
|
<div v-if="generateStatusVisible" class="generate-status-float">
|
||||||
|
<el-tooltip :content="generateStatusTooltip" placement="bottom">
|
||||||
|
<el-button class="generate-status-button" type="primary" plain size="small"
|
||||||
|
:disabled="activeGenerateTask?.status === 'CANCEL_REQUESTED'" @click="cancelActiveGenerateTask">
|
||||||
|
<span class="generate-progress-ring" :style="{ '--generate-progress': `${generateProgressDegree}deg` }" aria-hidden="true">
|
||||||
|
<span class="generate-progress-value">{{ generateProgressText }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="generate-status-text">
|
||||||
|
<strong>{{ generateStatusTitle }}</strong>
|
||||||
|
<small>{{ generateStatusSummary }}</small>
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-if="exportStatusVisible" class="export-status-float">
|
||||||
|
<el-tooltip content="点击查看正在生成的导出任务" placement="bottom">
|
||||||
|
<el-button class="export-status-button" type="primary" plain size="small" @click="openExportTask(activeExportTasks[0])">
|
||||||
|
<span class="export-progress-ring" aria-hidden="true">
|
||||||
|
<el-icon class="export-download-icon"><Download /></el-icon>
|
||||||
|
</span>
|
||||||
|
<span class="export-status-text">
|
||||||
|
<strong>导出中</strong>
|
||||||
|
<small>后台生成中{{ activeExportTasks.length > 1 ? ` · ${activeExportTasks.length}个任务` : '' }}</small>
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" :default-params="defaultSearchParams" row-key="arBillId" :selection="false">
|
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" :default-params="defaultSearchParams" row-key="arBillId" :selection="false">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
|
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
|
||||||
@@ -28,7 +55,7 @@
|
|||||||
|
|
||||||
<script setup name="ArBill">
|
<script setup name="ArBill">
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { listArBill, getArBill, confirmArBill, voidArBill, exportArBill, downloadArBillExport } from '@/api/ar/bill'
|
import { listArBill, getArBill, confirmArBill, voidArBill, exportArBill, getArBillExport, downloadArBillExport, getArBillGenerateTask, listActiveArBillGenerateTasks, cancelArBillGenerateTask, retryArBillGenerateTask } from '@/api/ar/bill'
|
||||||
import BillDetail from './components/BillDetail.vue'
|
import BillDetail from './components/BillDetail.vue'
|
||||||
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
import GenerateBillDialog from './components/GenerateBillDialog.vue'
|
||||||
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
import { AR_BILL_STATUS, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES } from '@/constants/ar/bill'
|
||||||
@@ -44,7 +71,52 @@ const defaultSearchParams = computed(() => ({
|
|||||||
const detailOpen = ref(false)
|
const detailOpen = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
|
const openedBillId = ref()
|
||||||
const generateOpen = ref(false)
|
const generateOpen = ref(false)
|
||||||
|
// 任务可能在首次轮询前就完成,至少展示一段时间让用户感知到“正在生成”。
|
||||||
|
const MIN_GENERATE_STATUS_VISIBLE_MS = 1800
|
||||||
|
// 每个任务独立保存轮询、收尾定时器和开始展示时间,避免多个账单任务互相覆盖。
|
||||||
|
const generatePollTimers = new Map()
|
||||||
|
const generateFinishTimers = new Map()
|
||||||
|
const generateVisibleSince = new Map()
|
||||||
|
const generateTasks = reactive(new Map())
|
||||||
|
const activeGenerateTasks = computed(() => Array.from(generateTasks.values()))
|
||||||
|
const activeGenerateTask = computed(() => activeGenerateTasks.value[0])
|
||||||
|
const generateStatusVisible = computed(() => activeGenerateTasks.value.length > 0)
|
||||||
|
const generateStatusTitle = computed(() => {
|
||||||
|
const status = activeGenerateTask.value?.status
|
||||||
|
if (status === 'WAITING') return '账单排队中'
|
||||||
|
if (status === 'CANCEL_REQUESTED') return '账单取消中'
|
||||||
|
return '账单生成中'
|
||||||
|
})
|
||||||
|
const generateStatusTooltip = computed(() => {
|
||||||
|
return activeGenerateTask.value?.status === 'CANCEL_REQUESTED'
|
||||||
|
? '正在清理已生成的账单数据,请稍候'
|
||||||
|
: '点击取消当前账单生成任务'
|
||||||
|
})
|
||||||
|
const generateProgressText = computed(() => {
|
||||||
|
const task = activeGenerateTask.value
|
||||||
|
if (!task || Number(task.totalCount) <= 0) return '…'
|
||||||
|
return `${Math.min(100, Math.floor(Number(task.processedCount || 0) * 100 / Number(task.totalCount)))}%`
|
||||||
|
})
|
||||||
|
const generateProgressDegree = computed(() => {
|
||||||
|
const task = activeGenerateTask.value
|
||||||
|
if (!task || Number(task.totalCount) <= 0) return 45
|
||||||
|
return Math.max(0, Math.min(360, Number(task.processedCount || 0) * 360 / Number(task.totalCount)))
|
||||||
|
})
|
||||||
|
const generateStatusSummary = computed(() => {
|
||||||
|
const task = activeGenerateTask.value
|
||||||
|
if (!task) return '任务提交中'
|
||||||
|
if (task.status === 'CANCEL_REQUESTED') return '正在清理数据'
|
||||||
|
if (task.status === 'WAITING') return '等待后台处理'
|
||||||
|
if (Number(task.totalCount) > 0) return `${Number(task.processedCount || 0)} / ${Number(task.totalCount)} 条`
|
||||||
|
return '正在准备数据'
|
||||||
|
})
|
||||||
|
const exportPollTimers = new Map()
|
||||||
|
const exportTasks = reactive(new Map())
|
||||||
|
const activeExportTasks = computed(() => Array.from(exportTasks.values()))
|
||||||
|
const exportSubmittingCount = ref(0)
|
||||||
|
const exportStatusVisible = computed(() => exportSubmittingCount.value > 0 || activeExportTasks.value.length > 0)
|
||||||
const statusOptions = AR_BILL_STATUS_OPTIONS
|
const statusOptions = AR_BILL_STATUS_OPTIONS
|
||||||
const customerScoped = computed(() => Boolean(route.query.customerId))
|
const customerScoped = computed(() => Boolean(route.query.customerId))
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -59,12 +131,167 @@ const searchColumns = [
|
|||||||
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' }
|
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 将页面日期范围转换为后端查询字段,其他筛选条件原样透传。
|
||||||
function queryList(params) { const query = { ...params }; if (query.billDateRange?.length === 2) { query.billDateStart = query.billDateRange[0]; query.billDateEnd = query.billDateRange[1] }; delete query.billDateRange; return listArBill(query) }
|
function queryList(params) { const query = { ...params }; if (query.billDateRange?.length === 2) { query.billDateStart = query.billDateRange[0]; query.billDateEnd = query.billDateRange[1] }; delete query.billDateRange; return listArBill(query) }
|
||||||
function handleGenerateSuccess(bill) { tableRef.value.refresh(); viewBill(bill) }
|
// 生成弹窗只提交任务,账单明细由后台异步批量写入;这里负责刷新列表并开始轮询。
|
||||||
function viewBill(row) { if (!row?.arBillId) return; detailOpen.value = true; detailLoading.value = true; detail.value = null; getArBill(row.arBillId).then(res => { detail.value = res.data || null }).finally(() => { detailLoading.value = false }) }
|
function handleGenerateSuccess(task) {
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
if (!task?.taskId) return
|
||||||
|
generateVisibleSince.set(task.taskId, Date.now())
|
||||||
|
generateTasks.set(task.taskId, task)
|
||||||
|
startGeneratePolling(task.taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGeneratePolling(taskId) {
|
||||||
|
stopGeneratePolling(taskId)
|
||||||
|
// WAITING/RUNNING/CANCEL_REQUESTED 都属于前端持续关注的中间状态。
|
||||||
|
const poll = () => {
|
||||||
|
getArBillGenerateTask(taskId).then(res => {
|
||||||
|
const task = res.data
|
||||||
|
if (!task) return
|
||||||
|
generateTasks.set(taskId, task)
|
||||||
|
if (['SUCCESS', 'FAILED', 'CANCELED'].includes(task.status)) {
|
||||||
|
finishGenerateTask(taskId, task)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
generatePollTimers.set(taskId, window.setTimeout(poll, 2000))
|
||||||
|
}).catch(() => {
|
||||||
|
generatePollTimers.set(taskId, window.setTimeout(poll, 3000))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
generatePollTimers.set(taskId, window.setTimeout(poll, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishGenerateTask(taskId, task) {
|
||||||
|
stopGeneratePolling(taskId)
|
||||||
|
if (generateFinishTimers.has(taskId)) return
|
||||||
|
// 即使后台很快返回 SUCCESS,也延迟清理浮层,保证用户能看到结果反馈。
|
||||||
|
const visibleSince = generateVisibleSince.get(taskId) || Date.now()
|
||||||
|
const remaining = Math.max(0, MIN_GENERATE_STATUS_VISIBLE_MS - (Date.now() - visibleSince))
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
generateFinishTimers.delete(taskId)
|
||||||
|
generateVisibleSince.delete(taskId)
|
||||||
|
generateTasks.delete(taskId)
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
if (task.status === 'SUCCESS') {
|
||||||
|
proxy.$modal.msgSuccess('账单生成成功')
|
||||||
|
if (task.arBillId) viewBill({ arBillId: task.arBillId })
|
||||||
|
} else if (task.status === 'FAILED') {
|
||||||
|
proxy.$modal.confirm(`账单生成失败:${task.errorMessage || '未知错误'},是否重新生成?`).then(() => retryGenerateTask(task.taskId)).then(res => {
|
||||||
|
const retryTask = res.data
|
||||||
|
if (retryTask?.taskId) {
|
||||||
|
generateVisibleSince.set(retryTask.taskId, Date.now())
|
||||||
|
generateTasks.set(retryTask.taskId, retryTask)
|
||||||
|
startGeneratePolling(retryTask.taskId)
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
} else {
|
||||||
|
proxy.$modal.msgWarning('账单生成任务已取消')
|
||||||
|
}
|
||||||
|
}, remaining)
|
||||||
|
generateFinishTimers.set(taskId, timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopGeneratePolling(taskId) {
|
||||||
|
const timer = generatePollTimers.get(taskId)
|
||||||
|
if (timer) window.clearTimeout(timer)
|
||||||
|
generatePollTimers.delete(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelActiveGenerateTask() {
|
||||||
|
const task = activeGenerateTask.value
|
||||||
|
if (!task?.taskId) return
|
||||||
|
proxy.$modal.confirm('账单正在后台生成,确定取消当前任务吗?').then(() => cancelArBillGenerateTask(task.taskId)).then(res => {
|
||||||
|
const updated = res.data
|
||||||
|
if (updated) generateTasks.set(updated.taskId, updated)
|
||||||
|
proxy.$modal.msgSuccess('已提交取消请求')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadActiveGenerateTasks() {
|
||||||
|
listActiveArBillGenerateTasks().then(res => {
|
||||||
|
;(res.data || []).forEach(task => {
|
||||||
|
generateTasks.set(task.taskId, task)
|
||||||
|
if (!generateVisibleSince.has(task.taskId)) generateVisibleSince.set(task.taskId, Date.now())
|
||||||
|
startGeneratePolling(task.taskId)
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
let detailRequestSerial = 0
|
||||||
|
function viewBill(row) {
|
||||||
|
if (!row?.arBillId) return
|
||||||
|
const serial = ++detailRequestSerial
|
||||||
|
openedBillId.value = row.arBillId
|
||||||
|
detailOpen.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
detail.value = null
|
||||||
|
getArBill(row.arBillId).then(res => {
|
||||||
|
if (serial === detailRequestSerial) detail.value = res.data || null
|
||||||
|
}).finally(() => {
|
||||||
|
if (serial === detailRequestSerial) detailLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
||||||
function voidBill(row) { proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' }).then(({ value }) => voidArBill(row.arBillId, { remark: value })).then(() => { proxy.$modal.msgSuccess('账单已作废'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
function voidBill(row) { proxy.$prompt(`请输入账单「${row.billNo}」的作废原因`, '作废账单', { inputPattern: /\S+/, inputErrorMessage: '作废原因不能为空' }).then(({ value }) => voidArBill(row.arBillId, { remark: value })).then(() => { proxy.$modal.msgSuccess('账单已作废'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
|
||||||
function exportBill(row) { if (!row?.arBillId) return; exportArBill(row.arBillId).then(res => { proxy.$modal.msgSuccess('导出成功'); tableRef.value.refresh(); if (detailOpen.value) viewBill(row); if (res.data?.exportId) downloadExport(res.data) }) }
|
function exportBill(row) {
|
||||||
|
if (!row?.arBillId) return
|
||||||
|
exportSubmittingCount.value += 1
|
||||||
|
exportArBill(row.arBillId).then(res => {
|
||||||
|
const exportTask = res.data
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
// 导出同样是异步任务;打开详情展示“生成中”状态及完成后的下载入口。
|
||||||
|
viewBill(row)
|
||||||
|
if (exportTask?.exportId) {
|
||||||
|
exportTasks.set(exportTask.exportId, exportTask)
|
||||||
|
startExportPolling(exportTask.exportId)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
exportSubmittingCount.value = Math.max(0, exportSubmittingCount.value - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadActiveGenerateTasks)
|
||||||
|
|
||||||
|
function startExportPolling(exportId) {
|
||||||
|
stopExportPolling(exportId)
|
||||||
|
// 导出文件在后台临时目录生成,前端只轮询状态,不等待文件内容返回。
|
||||||
|
const poll = () => {
|
||||||
|
getArBillExport(exportId).then(res => {
|
||||||
|
const exportTask = res.data
|
||||||
|
if (!exportTask || exportTask.exportStatus === 'PROCESSING') {
|
||||||
|
if (exportTask) exportTasks.set(exportId, exportTask)
|
||||||
|
exportPollTimers.set(exportId, window.setTimeout(poll, 1500))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopExportPolling(exportId)
|
||||||
|
exportTasks.delete(exportId)
|
||||||
|
tableRef.value?.refresh()
|
||||||
|
if (detailOpen.value && openedBillId.value === exportTask.arBillId) viewBill(exportTask)
|
||||||
|
if (exportTask.exportStatus === 'SUCCESS') {
|
||||||
|
proxy.$modal.msgSuccess('导出文件已生成,可在导出历史中下载')
|
||||||
|
} else {
|
||||||
|
proxy.$modal.msgError(`账单导出失败:${exportTask.remark || '请重新发起导出'}`)
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
stopExportPolling(exportId)
|
||||||
|
exportTasks.delete(exportId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
exportPollTimers.set(exportId, window.setTimeout(poll, 1500))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopExportPolling(exportId) {
|
||||||
|
const timer = exportPollTimers.get(exportId)
|
||||||
|
if (timer) window.clearTimeout(timer)
|
||||||
|
exportPollTimers.delete(exportId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExportTask(task) {
|
||||||
|
if (!task?.arBillId) return
|
||||||
|
viewBill({ arBillId: task.arBillId })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅下载 SUCCESS 状态的导出任务,文件名优先使用后端保存的名称。
|
||||||
function downloadExport(row) { downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`)) }
|
function downloadExport(row) { downloadArBillExport(row.exportId).then(blob => saveAs(blob, row.fileName || `账单_${row.exportId}.xlsx`)) }
|
||||||
function clearCustomerFilter() {
|
function clearCustomerFilter() {
|
||||||
const query = { ...route.query }
|
const query = { ...route.query }
|
||||||
@@ -72,7 +299,7 @@ function clearCustomerFilter() {
|
|||||||
router.replace({ path: route.path, query })
|
router.replace({ path: route.path, query })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从客户余额页重复进入不同客户时,路由组件会被复用,需同步更新表格内部查询条件
|
// 从客户余额页重复进入不同客户时,路由组件会被复用,需同步更新表格内部查询条件。
|
||||||
watch(() => route.query.customerId, (newId, oldId) => {
|
watch(() => route.query.customerId, (newId, oldId) => {
|
||||||
if (String(newId || '') === String(oldId || '')) return
|
if (String(newId || '') === String(oldId || '')) return
|
||||||
tableRef.value?.setQueryParams({
|
tableRef.value?.setQueryParams({
|
||||||
@@ -82,6 +309,18 @@ watch(() => route.query.customerId, (newId, oldId) => {
|
|||||||
tableRef.value?.handleQuery()
|
tableRef.value?.handleQuery()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 页面销毁时清理所有轮询定时器,避免切换菜单后仍访问旧页面状态。
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
generatePollTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
|
generatePollTimers.clear()
|
||||||
|
generateFinishTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
|
generateFinishTimers.clear()
|
||||||
|
generateVisibleSince.clear()
|
||||||
|
exportPollTimers.forEach(timer => window.clearTimeout(timer))
|
||||||
|
exportPollTimers.clear()
|
||||||
|
exportTasks.clear()
|
||||||
|
})
|
||||||
|
|
||||||
function canGoWriteoff(row) {
|
function canGoWriteoff(row) {
|
||||||
if (!row?.customerId) return false
|
if (!row?.customerId) return false
|
||||||
const canWriteoffStatus = row.status === AR_BILL_STATUS.CONFIRMED
|
const canWriteoffStatus = row.status === AR_BILL_STATUS.CONFIRMED
|
||||||
@@ -152,6 +391,136 @@ function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
|
|||||||
.ar-bill-page .written { color: #67c23a; }
|
.ar-bill-page .written { color: #67c23a; }
|
||||||
.ar-bill-page .unwritten { color: #e6a23c; }
|
.ar-bill-page .unwritten { color: #e6a23c; }
|
||||||
|
|
||||||
|
.ar-bill-page .generate-status-float {
|
||||||
|
position: fixed;
|
||||||
|
top: 92px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-status-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 184px;
|
||||||
|
min-height: 68px;
|
||||||
|
padding: 6px 16px 6px 7px;
|
||||||
|
border-radius: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-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(--generate-progress, 45deg),
|
||||||
|
var(--el-color-primary-light-8) var(--generate-progress, 45deg) 360deg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-progress-ring::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 5px;
|
||||||
|
content: '';
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-progress-value {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-status-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 3px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .generate-status-text strong { font-size: 14px; font-weight: 600; }
|
||||||
|
.ar-bill-page .generate-status-text small { color: var(--el-text-color-secondary); font-size: 11px; }
|
||||||
|
|
||||||
|
.ar-bill-page .export-status-float {
|
||||||
|
position: fixed;
|
||||||
|
top: 170px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 3001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-status-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 164px;
|
||||||
|
min-height: 62px;
|
||||||
|
border-radius: 32px;
|
||||||
|
padding: 6px 16px 6px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-progress-ring {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(var(--el-color-primary) 0deg 285deg, var(--el-color-primary-light-8) 285deg 360deg);
|
||||||
|
animation: ar-bill-export-spin 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-progress-ring::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 5px;
|
||||||
|
content: '';
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-download-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-status-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-status-text strong {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ar-bill-page .export-status-text small {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ar-bill-export-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.ar-bill-page :deep(.basic-table > .basic-form) {
|
.ar-bill-page :deep(.basic-table > .basic-form) {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ import {
|
|||||||
} from '@/constants/ar/enums'
|
} from '@/constants/ar/enums'
|
||||||
|
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
// 日志筛选值与后端枚举保持一致,展示文案统一由枚举工具转换。
|
||||||
const searchColumns = [
|
const searchColumns = [
|
||||||
{ label: '对象类型', prop: 'objectType', component: 'select', options: AR_LOG_OBJECT_TYPE_OPTIONS, width: '130px' },
|
{ label: '对象类型', prop: 'objectType', component: 'select', options: AR_LOG_OBJECT_TYPE_OPTIONS, width: '130px' },
|
||||||
{ label: '操作类型', prop: 'operationType', component: 'select', options: AR_LOG_OPERATION_TYPE_OPTIONS, width: '150px' },
|
{ label: '操作类型', prop: 'operationType', component: 'select', options: AR_LOG_OPERATION_TYPE_OPTIONS, width: '150px' },
|
||||||
{ label: '操作来源', prop: 'operationSource', component: 'select', options: AR_LOG_OPERATION_SOURCE_OPTIONS, width: '130px' },
|
{ label: '操作来源', prop: 'operationSource', component: 'select', options: AR_LOG_OPERATION_SOURCE_OPTIONS, width: '130px' },
|
||||||
{ label: '结果', prop: 'result', component: 'select', options: AR_LOG_RESULT_OPTIONS, width: '120px' }
|
{ label: '结果', prop: 'result', component: 'select', options: AR_LOG_RESULT_OPTIONS, width: '120px' }
|
||||||
]
|
]
|
||||||
|
// 状态变化列同时展示前后状态,便于追踪确认、作废和核销等生命周期操作。
|
||||||
const columns = [
|
const columns = [
|
||||||
{ label: '对象类型', prop: 'objectType', width: 110, formatter: row => objectTypeText(row.objectType) }, { label: '对象', prop: 'objectDisplay', minWidth: 220, showOverflowTooltip: true, formatter: row => row.objectDisplay || '-' },
|
{ label: '对象类型', prop: 'objectType', width: 110, formatter: row => objectTypeText(row.objectType) }, { label: '对象', prop: 'objectDisplay', minWidth: 220, showOverflowTooltip: true, formatter: row => row.objectDisplay || '-' },
|
||||||
{ label: '操作', prop: 'operationType', width: 150, formatter: row => operationText(row.operationType) }, { label: '说明', prop: 'operationDesc', minWidth: 220, showOverflowTooltip: true },
|
{ label: '操作', prop: 'operationType', width: 150, formatter: row => operationText(row.operationType) }, { label: '说明', prop: 'operationDesc', minWidth: 220, showOverflowTooltip: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user