|
|
@@ -199,6 +199,94 @@ public class OrderInvoiceService {
|
|
|
return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 商家出餐自动开票(010 Phase 11):读 pos_order 发票意图 → 组装 dto(个人 BuyerName=手机号、
|
|
|
+ * 捐赠=机构名)→ 复用开票核心 {@link #reIssueAndSave}。系统触发,不校验客户归属;货到付款未付款也开
|
|
|
+ * (送达失败/取消时作废兜底)。门店不可开票 / 订单无发票意图 / 已开 → 静默跳过,不影响出餐。
|
|
|
+ *
|
|
|
+ * @param orderId PosOrder.id(PK,与 invalid/retry/selectInvoiceDetail 一致)
|
|
|
+ * @return 1=已开 0=跳过或失败
|
|
|
+ */
|
|
|
+ public int autoIssue(Long orderId) {
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ if (order == null || StrUtil.isBlank(order.getInvoiceChoice())) {
|
|
|
+ return 0; // 订单不存在或无发票意图(门店不可开票时 createOrder 未写意图)
|
|
|
+ }
|
|
|
+ Long storeId = order.getMdId();
|
|
|
+ if (!canInvoice(storeId)) {
|
|
|
+ return 0; // 出餐时门店已不可开票(被停用等),跳过
|
|
|
+ }
|
|
|
+ PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
|
|
|
+ if (ez == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ // 防重复:已开则跳过
|
|
|
+ PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
|
|
|
+ if (row != null && Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ ApplyInvoiceDto dto = buildAutoIssueDto(order);
|
|
|
+ EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
|
|
|
+ // 捐赠=机构名;个人=手机号
|
|
|
+ String loveOrgName = null;
|
|
|
+ if ("LOVE_CODE".equals(order.getInvoiceChoice())) {
|
|
|
+ loveOrgName = resolveLoveOrg(dto.getLoveCode(), cfg);
|
|
|
+ dto.setBuyerName(loveOrgName);
|
|
|
+ } else if (isPersonalChoice(order.getInvoiceChoice())) {
|
|
|
+ InfoUser u = infoUserService.getOne(
|
|
|
+ new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, order.getUserId()));
|
|
|
+ dto.setBuyerName(u != null && StrUtil.isNotBlank(u.getPhone()) ? u.getPhone() : "會員");
|
|
|
+ }
|
|
|
+ PosOrderInvoice saveRow = row == null ? new PosOrderInvoice() : row;
|
|
|
+ saveRow.setOrderId(orderId);
|
|
|
+ saveRow.setOrderNo(order.getDdId());
|
|
|
+ saveRow.setStoreId(storeId);
|
|
|
+ saveRow.setInvoiceCategory(dto.getCategory());
|
|
|
+ saveRow.setBuyerName(dto.getBuyerName());
|
|
|
+ saveRow.setBuyerUbn(dto.getBuyerUbn());
|
|
|
+ saveRow.setBuyerEmail(dto.getBuyerEmail());
|
|
|
+ saveRow.setCarrierType(dto.getCarrierType());
|
|
|
+ saveRow.setCarrierNum(dto.getCarrierNum());
|
|
|
+ saveRow.setLoveCode(dto.getLoveCode());
|
|
|
+ saveRow.setLoveOrgName(loveOrgName);
|
|
|
+ saveRow.setApplyTime(new Date());
|
|
|
+ return reIssueAndSave(order, saveRow, dto, ez);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 出餐自动开票:pos_order 发票意图 → ApplyInvoiceDto(捐赠走 DONATION 复用既有分支)。 */
|
|
|
+ private ApplyInvoiceDto buildAutoIssueDto(PosOrder order) {
|
|
|
+ ApplyInvoiceDto dto = new ApplyInvoiceDto();
|
|
|
+ dto.setOrderId(order.getId());
|
|
|
+ String choice = order.getInvoiceChoice();
|
|
|
+ switch (choice) {
|
|
|
+ case "PAPER":
|
|
|
+ case "PHONE_BARCODE":
|
|
|
+ case "CITIZEN":
|
|
|
+ dto.setCategory("B2C");
|
|
|
+ dto.setCarrierType(order.getCarrierType()); // PAPER 时为 null → 个人纸本分支
|
|
|
+ dto.setCarrierNum(order.getCarrierNum());
|
|
|
+ break;
|
|
|
+ case "LOVE_CODE":
|
|
|
+ dto.setCategory("DONATION");
|
|
|
+ dto.setLoveCode(order.getLoveCode());
|
|
|
+ break;
|
|
|
+ case "COMPANY":
|
|
|
+ dto.setCategory("B2B");
|
|
|
+ dto.setBuyerUbn(order.getBuyerUbn());
|
|
|
+ dto.setBuyerName(order.getInvoiceBuyerName());
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isPersonalChoice(String choice) {
|
|
|
+ return "PAPER".equals(choice) || "PHONE_BARCODE".equals(choice) || "CITIZEN".equals(choice);
|
|
|
+ }
|
|
|
+
|
|
|
/** 客户查询订单发票;门店不可开票时 invoiceStatus=null(客户端据此隐藏入口)。 */
|
|
|
public PosOrderInvoiceVo getInvoice(Long orderId, Long currentUserId) {
|
|
|
PosOrder order = posOrderMapper.selectOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getDdId,orderId));
|
|
|
@@ -265,6 +353,11 @@ public class OrderInvoiceService {
|
|
|
return posOrderInvoiceMapper.selectInvoiceList(query);
|
|
|
}
|
|
|
|
|
|
+ /** 会员发票夹(010 Phase 11):按 userId 列已开发票(不含码图)。配合 PageHelper 分页。 */
|
|
|
+ public List<PosOrderInvoiceVo> listMyInvoices(Long userId) {
|
|
|
+ return posOrderInvoiceMapper.selectMyInvoices(userId);
|
|
|
+ }
|
|
|
+
|
|
|
/** 管理端:订单发票详情。 */
|
|
|
public PosOrderInvoiceVo detail(Long orderId) {
|
|
|
return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
@@ -425,6 +518,75 @@ public class OrderInvoiceService {
|
|
|
&& Integer.valueOf(1).equals(ez.getIsEnabled());
|
|
|
}
|
|
|
|
|
|
+ /** 取门店 ezPay 凭证配置;未开通/未启用返回 null(不抛异常,供校验场景容忍使用)。 */
|
|
|
+ private EzPayConfig getEzpayConfig(Long storeId) {
|
|
|
+ if (storeId == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
|
|
|
+ if (ez == null || !Integer.valueOf(2).equals(ez.getEzpayStatus())
|
|
|
+ || !Integer.valueOf(1).equals(ez.getIsEnabled())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 结账时校验发票意图(010 Phase 11):载具/捐赠联网验真、统编/凭证格式校验。
|
|
|
+ * PAPER(默认纸本)不校验。联网校验(手机条码/捐赠码)用 storeId 对应门店的 ezPay 凭证调 BDV。
|
|
|
+ *
|
|
|
+ * @param storeId 一个可开票门店(调用方负责选 canInvoice 的门店,用于取 ezPay 凭证)
|
|
|
+ */
|
|
|
+ public void validateCheckoutInvoiceIntent(String invoiceChoice, String carrierNum, String loveCode,
|
|
|
+ String buyerUbn, Long storeId) {
|
|
|
+ if (StrUtil.isBlank(invoiceChoice) || "PAPER".equals(invoiceChoice)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ switch (invoiceChoice) {
|
|
|
+ case "PHONE_BARCODE":
|
|
|
+ if (StrUtil.isBlank(carrierNum) || !carrierNum.matches("^/[0-9A-Z+\\-.]{7}$")) {
|
|
|
+ throw new ServiceException("手机条码格式不正确(须为 / 加 7 位大写英文/数字/+-.)");
|
|
|
+ }
|
|
|
+ EzPayConfig barCfg = getEzpayConfig(storeId);
|
|
|
+ if (barCfg == null) {
|
|
|
+ throw new ServiceException("门店暂不支持电子发票,无法校验手机条码");
|
|
|
+ }
|
|
|
+ boolean barExist;
|
|
|
+ try {
|
|
|
+ barExist = ezPay.checkBarCode(ezpayBaseUrl, barCfg, carrierNum);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ServiceException("手机条码校验服务暂不可用,请稍后重试");
|
|
|
+ }
|
|
|
+ if (!barExist) {
|
|
|
+ throw new ServiceException("手机条码不存在,请确认或改选纸本发票");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "CITIZEN":
|
|
|
+ if (StrUtil.isBlank(carrierNum) || !carrierNum.matches("^[A-Z]{2}\\d{14}$")) {
|
|
|
+ throw new ServiceException("自然人凭证格式不正确(须为 2 位大写英文 + 14 位数字)");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "LOVE_CODE":
|
|
|
+ if (StrUtil.isBlank(loveCode) || !loveCode.matches("^\\d{3,7}$")) {
|
|
|
+ throw new ServiceException("捐赠码格式不正确(须为 3-7 位数字)");
|
|
|
+ }
|
|
|
+ EzPayConfig loveCfg = getEzpayConfig(storeId);
|
|
|
+ if (loveCfg == null) {
|
|
|
+ throw new ServiceException("门店暂不支持电子发票,无法校验捐赠码");
|
|
|
+ }
|
|
|
+ resolveLoveOrg(loveCode, loveCfg); // 抛异常=捐赠码无效
|
|
|
+ break;
|
|
|
+ case "COMPANY":
|
|
|
+ if (StrUtil.isBlank(buyerUbn) || !buyerUbn.matches("^\\d{8}$")) {
|
|
|
+ throw new ServiceException("统一编号格式不正确(须为 8 位数字)");
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new ServiceException("发票类型不合法:" + invoiceChoice);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 捐赠机构列表(客户端选择器,仅 enabled=1,配合 PageHelper 分页)。
|
|
|
*/
|
|
|
@@ -490,8 +652,12 @@ public class OrderInvoiceService {
|
|
|
throw new ServiceException("买方名称不能为空");
|
|
|
}
|
|
|
String ct = dto.getCarrierType();
|
|
|
+ if (StrUtil.isBlank(ct)) {
|
|
|
+ // 无载具 → 个人纸本(PrintFlag=Y,零信息兜底),无需载具校验
|
|
|
+ return;
|
|
|
+ }
|
|
|
if (!"0".equals(ct) && !"1".equals(ct) && !"2".equals(ct)) {
|
|
|
- throw new ServiceException("B2C 发票须选择载具类型(0手机条码/1自然人凭证/2ezPay会员)");
|
|
|
+ throw new ServiceException("载具类型只能为 0手机条码/1自然人凭证/2ezPay会员");
|
|
|
}
|
|
|
if (StrUtil.isBlank(dto.getCarrierNum())) {
|
|
|
throw new ServiceException("载具号码不能为空");
|
|
|
@@ -537,13 +703,18 @@ public class OrderInvoiceService {
|
|
|
inv.put("LoveCode", dto.getLoveCode());
|
|
|
inv.put("PrintFlag", "N");
|
|
|
} else {
|
|
|
- // B2C 载具必填(validateInvoiceInput 已校验 carrierType∈0/1/2、号码非空),PrintFlag=N
|
|
|
- inv.put("CarrierType", dto.getCarrierType());
|
|
|
- inv.put("CarrierNum", dto.getCarrierNum());
|
|
|
- inv.put("PrintFlag", "N");
|
|
|
- // ezPay 会员载具(2)需邮箱;手机条码(0)/自然人凭证(1)票进载具,邮箱留空
|
|
|
- if ("2".equals(dto.getCarrierType()) && StrUtil.isNotBlank(dto.getBuyerEmail())) {
|
|
|
- inv.put("BuyerEmail", dto.getBuyerEmail());
|
|
|
+ // B2C:有载具 → PrintFlag=N 存载具;无载具 → PrintFlag=Y 个人纸本(零信息兜底,010 Phase 11)
|
|
|
+ if (StrUtil.isNotBlank(dto.getCarrierType())) {
|
|
|
+ inv.put("CarrierType", dto.getCarrierType());
|
|
|
+ inv.put("CarrierNum", dto.getCarrierNum());
|
|
|
+ inv.put("PrintFlag", "N");
|
|
|
+ // ezPay 会员载具(2)需邮箱;手机条码(0)/自然人凭证(1)票进载具,邮箱留空
|
|
|
+ if ("2".equals(dto.getCarrierType()) && StrUtil.isNotBlank(dto.getBuyerEmail())) {
|
|
|
+ inv.put("BuyerEmail", dto.getBuyerEmail());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 个人纸本:无载具无捐赠,法定 PrintFlag=Y;不传载具/邮箱
|
|
|
+ inv.put("PrintFlag", "Y");
|
|
|
}
|
|
|
}
|
|
|
inv.put("TaxType", "1");
|