|
@@ -7,6 +7,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.alibaba.excel.EasyExcel;
|
|
import com.alibaba.excel.EasyExcel;
|
|
|
import com.alibaba.excel.ExcelWriter;
|
|
import com.alibaba.excel.ExcelWriter;
|
|
|
import com.alibaba.excel.write.metadata.WriteSheet;
|
|
import com.alibaba.excel.write.metadata.WriteSheet;
|
|
|
|
|
+import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.domain.HotelTenantMapping;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.mapper.HotelTenantMappingMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.domain.HotelReconciliation;
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.domain.HotelReconciliation;
|
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.domain.HotelReconciliationItem;
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.domain.HotelReconciliationItem;
|
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.dto.ReconciliationGenerateDTO;
|
|
import com.mdframe.forge.business.core.hotel.reconciliation.dto.ReconciliationGenerateDTO;
|
|
@@ -29,8 +32,12 @@ import java.math.BigDecimal;
|
|
|
import java.net.URLEncoder;
|
|
import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.text.SimpleDateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
-import java.util.Calendar;
|
|
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
@@ -53,9 +60,12 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private HotelReconciliationItemMapper reconciliationItemMapper;
|
|
private HotelReconciliationItemMapper reconciliationItemMapper;
|
|
|
|
|
|
|
|
- private static final SimpleDateFormat MONTH_FMT = new SimpleDateFormat("yyyy-MM");
|
|
|
|
|
- private static final SimpleDateFormat MONTH_START_FMT = new SimpleDateFormat("yyyy-MM-01 00:00:00");
|
|
|
|
|
- private static final SimpleDateFormat NUMBER_FMT = new SimpleDateFormat("yyyyMM");
|
|
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private HotelTenantMappingMapper tenantMappingMapper;
|
|
|
|
|
+
|
|
|
|
|
+ /** 线程安全的日期格式化器(SimpleDateFormat 非线程安全,并发下会解析失败) */
|
|
|
|
|
+ private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
|
|
|
+ private static final DateTimeFormatter MONTH_START_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
|
|
|
// ==================== 查询类 ====================
|
|
// ==================== 查询类 ====================
|
|
|
|
|
|
|
@@ -67,8 +77,8 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public IPage<HotelReconciliationVO> partnerPage(PageQuery pageQuery, HotelReconciliation query) {
|
|
public IPage<HotelReconciliationVO> partnerPage(PageQuery pageQuery, HotelReconciliation query) {
|
|
|
- // 餐厅端跨租户访问:切到酒店租户(tenant_id=1)
|
|
|
|
|
- final Long hotelTenantId = 1L;
|
|
|
|
|
|
|
+ // 餐厅端跨租户访问:通过映射表动态解析酒店租户ID
|
|
|
|
|
+ final Long hotelTenantId = resolveHotelTenantForAdmin();
|
|
|
final Page<HotelReconciliationVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
|
|
final Page<HotelReconciliationVO> page = new Page<>(pageQuery.getPageNum(), pageQuery.getPageSize());
|
|
|
return TenantContextHolder.executeWithTenant(hotelTenantId, new java.util.function.Supplier<IPage<HotelReconciliationVO>>() {
|
|
return TenantContextHolder.executeWithTenant(hotelTenantId, new java.util.function.Supplier<IPage<HotelReconciliationVO>>() {
|
|
|
@Override
|
|
@Override
|
|
@@ -86,7 +96,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public ReconciliationDetailVO partnerDetail(Long id) {
|
|
public ReconciliationDetailVO partnerDetail(Long id) {
|
|
|
- final Long hotelTenantId = 1L;
|
|
|
|
|
|
|
+ final Long hotelTenantId = resolveHotelTenantForAdmin();
|
|
|
return TenantContextHolder.executeWithTenant(hotelTenantId, new java.util.function.Supplier<ReconciliationDetailVO>() {
|
|
return TenantContextHolder.executeWithTenant(hotelTenantId, new java.util.function.Supplier<ReconciliationDetailVO>() {
|
|
|
@Override
|
|
@Override
|
|
|
public ReconciliationDetailVO get() {
|
|
public ReconciliationDetailVO get() {
|
|
@@ -134,6 +144,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public void generate(ReconciliationGenerateDTO dto) {
|
|
public void generate(ReconciliationGenerateDTO dto) {
|
|
|
String yearMonth = dto.getYearMonth();
|
|
String yearMonth = dto.getYearMonth();
|
|
|
|
|
+ log.info("[对账单生成] 收到 yearMonth={}, class={}", yearMonth, yearMonth != null ? yearMonth.getClass().getName() : "null");
|
|
|
if (yearMonth == null || yearMonth.trim().isEmpty()) {
|
|
if (yearMonth == null || yearMonth.trim().isEmpty()) {
|
|
|
throw new BusinessException("请选择对账月份");
|
|
throw new BusinessException("请选择对账月份");
|
|
|
}
|
|
}
|
|
@@ -144,18 +155,16 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
String prefix = "DZ-" + yearMonth.replace("-", "") + "-";
|
|
String prefix = "DZ-" + yearMonth.replace("-", "") + "-";
|
|
|
String existMaxNo = reconciliationMapper.selectMaxNo(tenantId, prefix);
|
|
String existMaxNo = reconciliationMapper.selectMaxNo(tenantId, prefix);
|
|
|
|
|
|
|
|
- // 计算月份起止时间
|
|
|
|
|
|
|
+ // 计算月份起止时间(LocalDate 无时间字段,需转 LocalDateTime 再格式化)
|
|
|
String startTime;
|
|
String startTime;
|
|
|
String endTime;
|
|
String endTime;
|
|
|
try {
|
|
try {
|
|
|
- Date monthDate = MONTH_FMT.parse(yearMonth);
|
|
|
|
|
- Calendar cal = Calendar.getInstance();
|
|
|
|
|
- cal.setTime(monthDate);
|
|
|
|
|
- startTime = MONTH_START_FMT.format(cal.getTime());
|
|
|
|
|
|
|
+ LocalDate firstDay = LocalDate.parse(yearMonth + "-01", DateTimeFormatter.ISO_LOCAL_DATE);
|
|
|
|
|
+ startTime = firstDay.atStartOfDay().format(MONTH_START_FMT);
|
|
|
// 下个月1号 00:00:00 作为结束边界(不含)
|
|
// 下个月1号 00:00:00 作为结束边界(不含)
|
|
|
- cal.add(Calendar.MONTH, 1);
|
|
|
|
|
- endTime = MONTH_START_FMT.format(cal.getTime());
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
|
|
+ LocalDate nextMonthFirst = firstDay.plusMonths(1);
|
|
|
|
|
+ endTime = nextMonthFirst.atStartOfDay().format(MONTH_START_FMT);
|
|
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
throw new BusinessException("月份格式错误,请使用 yyyy-MM 格式");
|
|
throw new BusinessException("月份格式错误,请使用 yyyy-MM 格式");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -187,13 +196,11 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
Date periodStart;
|
|
Date periodStart;
|
|
|
Date periodEnd;
|
|
Date periodEnd;
|
|
|
try {
|
|
try {
|
|
|
- periodStart = MONTH_FMT.parse(yearMonth);
|
|
|
|
|
- Calendar endCal = Calendar.getInstance();
|
|
|
|
|
- endCal.setTime(periodStart);
|
|
|
|
|
- endCal.add(Calendar.MONTH, 1);
|
|
|
|
|
- endCal.add(Calendar.DAY_OF_MONTH, -1);
|
|
|
|
|
- periodEnd = endCal.getTime();
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
|
|
+ LocalDate firstDay = LocalDate.parse(yearMonth + "-01", DateTimeFormatter.ISO_LOCAL_DATE);
|
|
|
|
|
+ periodStart = Date.from(firstDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ LocalDate lastDay = firstDay.plusMonths(1).minusDays(1);
|
|
|
|
|
+ periodEnd = Date.from(lastDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ } catch (DateTimeParseException e) {
|
|
|
// 不会走到这里,前面已经校验过格式
|
|
// 不会走到这里,前面已经校验过格式
|
|
|
throw new BusinessException("月份格式错误");
|
|
throw new BusinessException("月份格式错误");
|
|
|
}
|
|
}
|
|
@@ -292,12 +299,12 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
public void submit(Long id) {
|
|
public void submit(Long id) {
|
|
|
HotelReconciliation reconciliation = requireReconciliation(id);
|
|
HotelReconciliation reconciliation = requireReconciliation(id);
|
|
|
if (reconciliation.getStatus() != 0) {
|
|
if (reconciliation.getStatus() != 0) {
|
|
|
- throw new BusinessException("只有待确认状态的对账单才能提交");
|
|
|
|
|
|
|
+ throw new BusinessException("只有待提交状态的对账单才能提交");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
|
updateWrapper.eq("id", id);
|
|
updateWrapper.eq("id", id);
|
|
|
- updateWrapper.set("status", 1); // 已确认(提交后餐厅可见)
|
|
|
|
|
|
|
+ updateWrapper.set("status", 1); // 待确认(提交后餐厅可见)
|
|
|
reconciliationMapper.update(null, updateWrapper);
|
|
reconciliationMapper.update(null, updateWrapper);
|
|
|
|
|
|
|
|
log.info("对账单已提交: id={}, no={}", id, reconciliation.getReconciliationNo());
|
|
log.info("对账单已提交: id={}, no={}", id, reconciliation.getReconciliationNo());
|
|
@@ -305,7 +312,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public void confirm(Long id) {
|
|
public void confirm(Long id) {
|
|
|
- final Long hotelTenantId = 1L;
|
|
|
|
|
|
|
+ final Long hotelTenantId = resolveHotelTenantForAdmin();
|
|
|
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
|
|
TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
|
|
|
@Override
|
|
@Override
|
|
|
public void run() {
|
|
public void run() {
|
|
@@ -314,13 +321,14 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
throw new BusinessException("对账单不存在");
|
|
throw new BusinessException("对账单不存在");
|
|
|
}
|
|
}
|
|
|
if (reconciliation.getStatus() != 1) {
|
|
if (reconciliation.getStatus() != 1) {
|
|
|
- throw new BusinessException("只有已确认状态的对账单才能确认");
|
|
|
|
|
|
|
+ throw new BusinessException("只有待确认状态的对账单才能确认");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Long currentUserId = SessionHelper.getUserId();
|
|
Long currentUserId = SessionHelper.getUserId();
|
|
|
|
|
|
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
|
updateWrapper.eq("id", id);
|
|
updateWrapper.eq("id", id);
|
|
|
|
|
+ updateWrapper.set("status", 2); // 已确认(餐厅确认后)
|
|
|
updateWrapper.set("confirmed_by", currentUserId);
|
|
updateWrapper.set("confirmed_by", currentUserId);
|
|
|
updateWrapper.set("confirmed_time", new Date());
|
|
updateWrapper.set("confirmed_time", new Date());
|
|
|
reconciliationMapper.update(null, updateWrapper);
|
|
reconciliationMapper.update(null, updateWrapper);
|
|
@@ -333,7 +341,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
@Override
|
|
@Override
|
|
|
public void settle(Long id, String remark) {
|
|
public void settle(Long id, String remark) {
|
|
|
HotelReconciliation reconciliation = requireReconciliation(id);
|
|
HotelReconciliation reconciliation = requireReconciliation(id);
|
|
|
- if (reconciliation.getStatus() != 1) {
|
|
|
|
|
|
|
+ if (reconciliation.getStatus() != 2) {
|
|
|
throw new BusinessException("只有已确认状态的对账单才能标记结算");
|
|
throw new BusinessException("只有已确认状态的对账单才能标记结算");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -341,7 +349,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
|
|
|
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
UpdateWrapper<HotelReconciliation> updateWrapper = new UpdateWrapper<>();
|
|
|
updateWrapper.eq("id", id);
|
|
updateWrapper.eq("id", id);
|
|
|
- updateWrapper.set("status", 2); // 已结算
|
|
|
|
|
|
|
+ updateWrapper.set("status", 3); // 已结算
|
|
|
updateWrapper.set("settled_by", currentUserId);
|
|
updateWrapper.set("settled_by", currentUserId);
|
|
|
updateWrapper.set("settled_time", new Date());
|
|
updateWrapper.set("settled_time", new Date());
|
|
|
updateWrapper.set("settled_remark", remark);
|
|
updateWrapper.set("settled_remark", remark);
|
|
@@ -388,15 +396,32 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
// 状态文本映射
|
|
// 状态文本映射
|
|
|
String statusText;
|
|
String statusText;
|
|
|
if (detail.getStatus() == 0) {
|
|
if (detail.getStatus() == 0) {
|
|
|
- statusText = "待确认";
|
|
|
|
|
|
|
+ statusText = "待提交";
|
|
|
} else if (detail.getStatus() == 1) {
|
|
} else if (detail.getStatus() == 1) {
|
|
|
- statusText = "已确认";
|
|
|
|
|
|
|
+ statusText = "待确认";
|
|
|
} else if (detail.getStatus() == 2) {
|
|
} else if (detail.getStatus() == 2) {
|
|
|
|
|
+ statusText = "已确认";
|
|
|
|
|
+ } else if (detail.getStatus() == 3) {
|
|
|
statusText = "已结算";
|
|
statusText = "已结算";
|
|
|
} else {
|
|
} else {
|
|
|
statusText = String.valueOf(detail.getStatus());
|
|
statusText = String.valueOf(detail.getStatus());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 提取年月用于标题(从 reconciliationNo 如 DZ-202609-005 提取 202609)
|
|
|
|
|
+ String yearMonthLabel = "";
|
|
|
|
|
+ String reconNo = detail.getReconciliationNo();
|
|
|
|
|
+ if (reconNo != null && reconNo.startsWith("DZ-")) {
|
|
|
|
|
+ int dashIdx = reconNo.indexOf('-', 3);
|
|
|
|
|
+ if (dashIdx > 0) {
|
|
|
|
|
+ String ym = reconNo.substring(3, dashIdx); // 202609
|
|
|
|
|
+ if (ym.length() == 6) {
|
|
|
|
|
+ yearMonthLabel = ym.substring(0, 4) + "年" + ym.substring(4, 6) + "月";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ String summaryTitleText = yearMonthLabel + "对账单汇总";
|
|
|
|
|
+ String itemTitleText = yearMonthLabel + "订单明细清单";
|
|
|
|
|
+
|
|
|
// ===== Sheet1: 对账汇总 =====
|
|
// ===== Sheet1: 对账汇总 =====
|
|
|
List<List<String>> summaryHeaders = new ArrayList<List<String>>();
|
|
List<List<String>> summaryHeaders = new ArrayList<List<String>>();
|
|
|
summaryHeaders.add(Collections.singletonList("对账单编号"));
|
|
summaryHeaders.add(Collections.singletonList("对账单编号"));
|
|
@@ -413,6 +438,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
summaryHeaders.add(Collections.singletonList("结算备注"));
|
|
summaryHeaders.add(Collections.singletonList("结算备注"));
|
|
|
|
|
|
|
|
List<List<Object>> summaryRows = new ArrayList<List<Object>>();
|
|
List<List<Object>> summaryRows = new ArrayList<List<Object>>();
|
|
|
|
|
+ // 数据行(不再包含标题行)
|
|
|
List<Object> row = new ArrayList<Object>();
|
|
List<Object> row = new ArrayList<Object>();
|
|
|
row.add(detail.getReconciliationNo());
|
|
row.add(detail.getReconciliationNo());
|
|
|
row.add(formatDateStr(detail.getPeriodStart()) + " ~ " + formatDateStr(detail.getPeriodEnd()));
|
|
row.add(formatDateStr(detail.getPeriodStart()) + " ~ " + formatDateStr(detail.getPeriodEnd()));
|
|
@@ -440,6 +466,7 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
itemHeaders.add(Collections.singletonList("排除原因"));
|
|
itemHeaders.add(Collections.singletonList("排除原因"));
|
|
|
|
|
|
|
|
List<List<Object>> itemRows = new ArrayList<List<Object>>();
|
|
List<List<Object>> itemRows = new ArrayList<List<Object>>();
|
|
|
|
|
+ // 数据行(不再包含标题行)
|
|
|
List<ReconciliationDetailVO.ReconciliationItemVO> items = detail.getItems();
|
|
List<ReconciliationDetailVO.ReconciliationItemVO> items = detail.getItems();
|
|
|
if (items != null) {
|
|
if (items != null) {
|
|
|
for (int i = 0; i < items.size(); i++) {
|
|
for (int i = 0; i < items.size(); i++) {
|
|
@@ -450,7 +477,6 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
itemRow.add(item.getRoomNo());
|
|
itemRow.add(item.getRoomNo());
|
|
|
itemRow.add(item.getContactName());
|
|
itemRow.add(item.getContactName());
|
|
|
itemRow.add(item.getPayAmount());
|
|
itemRow.add(item.getPayAmount());
|
|
|
- // 退款状态文本
|
|
|
|
|
if (item.getRefundStatus() != null && item.getRefundStatus() == 1) {
|
|
if (item.getRefundStatus() != null && item.getRefundStatus() == 1) {
|
|
|
itemRow.add("已退款");
|
|
itemRow.add("已退款");
|
|
|
} else if (item.getRefundStatus() != null && item.getRefundStatus() == 2) {
|
|
} else if (item.getRefundStatus() != null && item.getRefundStatus() == 2) {
|
|
@@ -464,24 +490,53 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ===== 写入 Excel =====
|
|
|
|
|
|
|
+ // ===== 写入 Excel(先写内存,再用 POI 插入标题行)=====
|
|
|
String fileName = "对账单_" + detail.getReconciliationNo() + ".xlsx";
|
|
String fileName = "对账单_" + detail.getReconciliationNo() + ".xlsx";
|
|
|
try {
|
|
try {
|
|
|
- response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
|
|
- response.setCharacterEncoding("utf-8");
|
|
|
|
|
- String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
|
|
|
|
|
- response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
|
|
|
|
|
-
|
|
|
|
|
- ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
|
|
|
|
|
|
|
+ // 1. 先用 EasyExcel 写到 ByteArrayOutputStream
|
|
|
|
|
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
|
|
|
+ ExcelWriter excelWriter = EasyExcel.write(baos).build();
|
|
|
try {
|
|
try {
|
|
|
- WriteSheet summarySheet = EasyExcel.writerSheet(0, "对账汇总").head(summaryHeaders).build();
|
|
|
|
|
|
|
+ WriteSheet summarySheet = EasyExcel.writerSheet(0, "对账汇总")
|
|
|
|
|
+ .head(summaryHeaders)
|
|
|
|
|
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
|
|
|
|
+ .build();
|
|
|
excelWriter.write(summaryRows, summarySheet);
|
|
excelWriter.write(summaryRows, summarySheet);
|
|
|
|
|
|
|
|
- WriteSheet itemSheet = EasyExcel.writerSheet(1, "订单明细").head(itemHeaders).build();
|
|
|
|
|
|
|
+ WriteSheet itemSheet = EasyExcel.writerSheet(1, "订单明细")
|
|
|
|
|
+ .head(itemHeaders)
|
|
|
|
|
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
|
|
|
|
+ .build();
|
|
|
excelWriter.write(itemRows, itemSheet);
|
|
excelWriter.write(itemRows, itemSheet);
|
|
|
} finally {
|
|
} finally {
|
|
|
excelWriter.finish();
|
|
excelWriter.finish();
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 用 POI 打开内存中的工作簿,在每个 Sheet 第0行插入标题
|
|
|
|
|
+ byte[] excelBytes = baos.toByteArray();
|
|
|
|
|
+ baos.close();
|
|
|
|
|
+
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
|
|
|
|
|
+ new org.apache.poi.xssf.usermodel.XSSFWorkbook(new java.io.ByteArrayInputStream(excelBytes));
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Sheet1: 对账汇总标题
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Sheet sheet1 = workbook.getSheetAt(0);
|
|
|
|
|
+ insertTitleRow(sheet1, summaryTitleText, 11);
|
|
|
|
|
+
|
|
|
|
|
+ // Sheet2: 订单明细标题
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Sheet sheet2 = workbook.getSheetAt(1);
|
|
|
|
|
+ insertTitleRow(sheet2, itemTitleText, 7);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 写出到响应
|
|
|
|
|
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
|
|
+ response.setCharacterEncoding("utf-8");
|
|
|
|
|
+ String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
|
|
|
|
|
+ response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
|
|
|
|
|
+
|
|
|
|
|
+ workbook.write(response.getOutputStream());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ workbook.close();
|
|
|
|
|
+ }
|
|
|
} catch (java.io.IOException e) {
|
|
} catch (java.io.IOException e) {
|
|
|
throw new BusinessException("导出对账单失败: " + e.getMessage());
|
|
throw new BusinessException("导出对账单失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
@@ -490,6 +545,133 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
|
|
+ * 在 Sheet 第0行插入标题并合并单元格,同时美化表头和数据行样式(参考 EasyPoi 风格)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void insertTitleRow(org.apache.poi.ss.usermodel.Sheet sheet, String title, int lastColIndex) {
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFWorkbook workbook =
|
|
|
|
|
+ (org.apache.poi.xssf.usermodel.XSSFWorkbook) sheet.getWorkbook();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 样式定义 =====
|
|
|
|
|
+ // 标题样式:16pt 加粗、居中
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFCellStyle titleStyle = workbook.createCellStyle();
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFFont titleFont = workbook.createFont();
|
|
|
|
|
+ titleFont.setBold(true);
|
|
|
|
|
+ titleFont.setFontHeightInPoints((short) 16);
|
|
|
|
|
+ titleStyle.setFont(titleFont);
|
|
|
|
|
+ titleStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
|
|
|
|
|
+ titleStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
|
|
|
|
|
+
|
|
|
|
|
+ // 表头样式:11pt 加粗、居中、浅蓝背景、细边框
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
|
|
|
|
|
+ headerFont.setBold(true);
|
|
|
|
|
+ headerFont.setFontHeightInPoints((short) 11);
|
|
|
|
|
+ headerStyle.setFont(headerFont);
|
|
|
|
|
+ headerStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
|
|
|
|
|
+ headerStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
|
|
|
|
|
+ headerStyle.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(
|
|
|
|
|
+ new byte[]{(byte) 0xD9, (byte) 0xE1, (byte) 0xF2}, null));
|
|
|
|
|
+ headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
|
|
|
|
|
+ setThinBorder(headerStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行样式:10pt、细边框
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFCellStyle dataStyle = workbook.createCellStyle();
|
|
|
|
|
+ org.apache.poi.xssf.usermodel.XSSFFont dataFont = workbook.createFont();
|
|
|
|
|
+ dataFont.setFontHeightInPoints((short) 10);
|
|
|
|
|
+ dataStyle.setFont(dataFont);
|
|
|
|
|
+ dataStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
|
|
|
|
|
+ setThinBorder(dataStyle);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 将现有所有行下移一行 =====
|
|
|
|
|
+ for (int i = sheet.getLastRowNum(); i >= 0; i--) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Row srcRow = sheet.getRow(i);
|
|
|
|
|
+ if (srcRow != null) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Row newRow = sheet.createRow(i + 1);
|
|
|
|
|
+ for (int j = 0; j < srcRow.getLastCellNum(); j++) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Cell srcCell = srcRow.getCell(j);
|
|
|
|
|
+ if (srcCell != null) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Cell newCell = newRow.createCell(j);
|
|
|
|
|
+ switch (srcCell.getCellType()) {
|
|
|
|
|
+ case STRING:
|
|
|
|
|
+ newCell.setCellValue(srcCell.getStringCellValue());
|
|
|
|
|
+ break;
|
|
|
|
|
+ case NUMERIC:
|
|
|
|
|
+ if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(srcCell)) {
|
|
|
|
|
+ // 日期单元格:格式化为字符串,避免显示为 Excel 序列号
|
|
|
|
|
+ newCell.setCellValue(formatDateStr(srcCell.getDateCellValue()));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ newCell.setCellValue(srcCell.getNumericCellValue());
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ case BOOLEAN:
|
|
|
|
|
+ newCell.setCellValue(srcCell.getBooleanCellValue());
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ newCell.setCellValue(srcCell.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ sheet.removeRow(srcRow);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 第0行:标题行 =====
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Row titleRow = sheet.createRow(0);
|
|
|
|
|
+ titleRow.setHeightInPoints(30);
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Cell titleCell = titleRow.createCell(0);
|
|
|
|
|
+ titleCell.setCellValue(title);
|
|
|
|
|
+ titleCell.setCellStyle(titleStyle);
|
|
|
|
|
+ sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, lastColIndex));
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 第1行:表头行样式 =====
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Row headerRow = sheet.getRow(1);
|
|
|
|
|
+ if (headerRow != null) {
|
|
|
|
|
+ headerRow.setHeightInPoints(22);
|
|
|
|
|
+ for (int j = 0; j < headerRow.getLastCellNum(); j++) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Cell cell = headerRow.getCell(j);
|
|
|
|
|
+ if (cell != null) {
|
|
|
|
|
+ cell.setCellStyle(headerStyle);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 第2行起:数据行样式 =====
|
|
|
|
|
+ for (int i = 2; i <= sheet.getLastRowNum(); i++) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Row dataRow = sheet.getRow(i);
|
|
|
|
|
+ if (dataRow != null) {
|
|
|
|
|
+ dataRow.setHeightInPoints(20);
|
|
|
|
|
+ for (int j = 0; j < dataRow.getLastCellNum(); j++) {
|
|
|
|
|
+ org.apache.poi.ss.usermodel.Cell cell = dataRow.getCell(j);
|
|
|
|
|
+ if (cell != null) {
|
|
|
|
|
+ cell.setCellStyle(dataStyle);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置单元格细边框。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void setThinBorder(org.apache.poi.xssf.usermodel.XSSFCellStyle style) {
|
|
|
|
|
+ style.setBorderTop(org.apache.poi.ss.usermodel.BorderStyle.THIN);
|
|
|
|
|
+ style.setBorderBottom(org.apache.poi.ss.usermodel.BorderStyle.THIN);
|
|
|
|
|
+ style.setBorderLeft(org.apache.poi.ss.usermodel.BorderStyle.THIN);
|
|
|
|
|
+ style.setBorderRight(org.apache.poi.ss.usermodel.BorderStyle.THIN);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void partnerExportSingle(Long id, HttpServletResponse response) {
|
|
|
|
|
+ final Long hotelTenantId = resolveHotelTenantForAdmin();
|
|
|
|
|
+ TenantContextHolder.executeWithTenant(hotelTenantId, new Runnable() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void run() {
|
|
|
|
|
+ exportSingle(id, response);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
* 日期格式化为 yyyy-MM-dd。
|
|
* 日期格式化为 yyyy-MM-dd。
|
|
|
*/
|
|
*/
|
|
|
private String formatDateStr(Date date) {
|
|
private String formatDateStr(Date date) {
|
|
@@ -518,6 +700,19 @@ public class HotelReconciliationServiceImpl implements HotelReconciliationServic
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
|
|
+ * 管理端反查酒店租户ID。
|
|
|
|
|
+ * 餐厅租户登录时,通过映射表找到对应的酒店租户;酒店租户登录时,直接返回当前租户。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Long resolveHotelTenantForAdmin() {
|
|
|
|
|
+ Long currentTenantId = resolveTenantId();
|
|
|
|
|
+ HotelTenantMapping mapping = tenantMappingMapper.selectByRestaurantTenantId(currentTenantId);
|
|
|
|
|
+ if (mapping != null && mapping.getHotelTenantId() != null) {
|
|
|
|
|
+ return mapping.getHotelTenantId();
|
|
|
|
|
+ }
|
|
|
|
|
+ return currentTenantId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
* 查找对账单,不存在则抛异常。
|
|
* 查找对账单,不存在则抛异常。
|
|
|
*/
|
|
*/
|
|
|
private HotelReconciliation requireReconciliation(Long id) {
|
|
private HotelReconciliation requireReconciliation(Long id) {
|