账单异步生成优化

This commit is contained in:
caojia
2026-09-20 16:42:11 +08:00
parent 8d1abf0eb1
commit 85b37bc07e
52 changed files with 3948 additions and 790 deletions
@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.ar;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
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 com.ruoyi.ar.domain.ArBill;
import com.ruoyi.ar.domain.ArBankAccount;
import com.ruoyi.ar.domain.ArBillDetailFee;
import com.ruoyi.ar.domain.ArBillExport;
import com.ruoyi.ar.domain.ArBillGenerateTask;
import com.ruoyi.ar.domain.ArBillLog;
import com.ruoyi.ar.domain.vo.ArBillActionRequest;
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.price.domain.PrcOrderBill;
/** 应收账单管理。 */
/**
* 应收账单 HTTP 接口层。
*
* <p>负责账单列表、账单生成、确认/作废、异步导出、日志和详情分页接口;
* 具体业务规则统一下沉到 {@code IArBillService}。</p>
*/
@RestController
@RequestMapping("/ar/bill")
@Validated
public class ArBillController extends BaseController
{
public class ArBillController extends BaseController {
@Autowired
private IArBillService billService;
/**
* 查询账单列表;分页参数由 BaseController.startPage 交给 MyBatis PageHelper 处理。
*/
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
@GetMapping("/list")
public TableDataInfo list(ArBillQuery query)
{
public TableDataInfo list(ArBillQuery query) {
startPage();
List<ArBill> list = billService.selectArBillList(query);
return getDataTable(list);
}
/**
* 查询账单头信息;账单明细通过独立分页接口按需加载。
*/
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
@GetMapping("/{arBillId}")
public AjaxResult getInfo(@PathVariable Long arBillId)
{
public AjaxResult getInfo(@PathVariable Long 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')")
@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);
return success(account);
}
/** 查询生成账单前的订单计费结果。 */
/**
* 查询生成账单前的订单计费结果。
*/
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
@GetMapping("/generate/preview")
public TableDataInfo preview(ArBillGenerateQuery query)
{
public AjaxResult preview(ArBillGenerateQuery query) {
startPage();
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)
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
@PostMapping("/generate")
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);
ArBill bill = billService.generateBill(request, getUsername(), requestId);
return success(bill).put("request_id", requestId);
ArBillGenerateTask task = billService.generateBill(request, getUsername(), 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)
@PreAuthorize("@ss.hasPermi('ar:bill:confirm')")
@PutMapping("/{arBillId}/confirm")
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);
return success(billService.confirmBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
}
/**
* 作废尚未发生核销的账单;账单明细保留作为历史快照。
*/
@Log(title = "应收账单", businessType = BusinessType.UPDATE)
@PreAuthorize("@ss.hasPermi('ar:bill:void')")
@PutMapping("/{arBillId}/void")
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);
return success(billService.voidBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId);
}
/**
* 仅提交导出任务;前端通过导出任务状态接口轮询并在完成后下载。
*/
@Log(title = "应收账单导出", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('ar:bill:export')")
@PostMapping("/{arBillId}/export")
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);
return success(billService.exportBill(arBillId, getUsername(), requestId)).put("request_id", requestId);
}
/**
* 查询当前账单的导出历史和任务状态。
*/
@PreAuthorize("@ss.hasPermi('ar:bill:query')")
@GetMapping("/{arBillId}/exports")
public AjaxResult exports(@PathVariable Long arBillId)
{
public AjaxResult exports(@PathVariable Long 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')")
@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);
}
/**
* 分页查询账单操作日志。
*/
@PreAuthorize("@ss.hasPermi('ar:log:list')")
@GetMapping("/log/list")
public TableDataInfo logs(ArBillLog query)
{
public TableDataInfo logs(ArBillLog query) {
startPage();
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(headerRequestId)) return headerRequestId;
return IdUtils.fastSimpleUUID();
@@ -29,8 +29,8 @@ spring:
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置间隔多久检测一次空闲连接,避免远程数据库断开后连接池继续复用失效连接,单位是毫秒
timeBetweenEvictionRunsMillis: 30000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
@@ -38,7 +38,8 @@ spring:
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
# 异步账单任务借用连接时校验连接有效性,避免拿到已被数据库断开的连接
testOnBorrow: true
testOnReturn: false
webStatFilter:
enabled: true
@@ -59,4 +60,4 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
multi-statement-allow: true