|
|
@@ -0,0 +1,522 @@
|
|
|
+package com.ruoyi.app.order;
|
|
|
+
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.ruoyi.app.utils.ezPay.EzPay;
|
|
|
+import com.ruoyi.app.utils.ezPay.EzPayConfig;
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
+import com.ruoyi.common.utils.SecurityUtils;
|
|
|
+import com.ruoyi.system.domain.PosOrder;
|
|
|
+import com.ruoyi.system.domain.PosOrderInvoice;
|
|
|
+import com.ruoyi.system.domain.PosStore;
|
|
|
+import com.ruoyi.system.domain.PosStoreEzpay;
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
+import com.ruoyi.system.domain.dto.ApplyInvoiceDto;
|
|
|
+import com.ruoyi.system.domain.vo.PosOrderInvoiceVo;
|
|
|
+import com.ruoyi.system.mapper.PosOrderInvoiceMapper;
|
|
|
+import com.ruoyi.system.mapper.PosOrderMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreEzpayMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreMapper;
|
|
|
+import com.ruoyi.system.service.IInfoUserService;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 订单电子发票 开票 / 作废 / 查询 业务(放 admin,因需调用 {@link EzPay})。
|
|
|
+ *
|
|
|
+ * <p>开票链路({@link #applyInvoice}):校验订单归属/状态/支付 → 校验门店可开票(009 凭证 + 免用发票)
|
|
|
+ * → 金额拆分(不含运费)→ 组装 ezPay issue 参数 + 逐商品明细 → 调 {@link EzPay#issueInvoice}
|
|
|
+ * → 判读回应 → 落库。作废走 {@link EzPay#doPost} + invoice_invalid。
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ * @date 2026-06-16
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class OrderInvoiceService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private EzPay ezPay;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PosOrderMapper posOrderMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PosStoreMapper posStoreMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PosStoreEzpayMapper posStoreEzpayMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PosOrderInvoiceMapper posOrderInvoiceMapper;
|
|
|
+ @Autowired
|
|
|
+ private IInfoUserService infoUserService;
|
|
|
+
|
|
|
+ /** ezPay 接口根地址(从 application.yml 的 ezpay.base-url 注入,测试/正式在此切换) */
|
|
|
+ @Value("${ezpay.base-url}")
|
|
|
+ private String ezpayBaseUrl;
|
|
|
+
|
|
|
+ /** 发票状态:0未开/1已开/2失败/3作废 */
|
|
|
+ private static final int STATUS_NOT_ISSUED = 0;
|
|
|
+ private static final int STATUS_ISSUED = 1;
|
|
|
+ private static final int STATUS_FAILED = 2;
|
|
|
+ private static final int STATUS_INVALID = 3;
|
|
|
+
|
|
|
+ /** 订单完成态、已支付 */
|
|
|
+ private static final long ORDER_STATE_DONE = 3L;
|
|
|
+ private static final long ORDER_PAID = 1L;
|
|
|
+
|
|
|
+ // ==================== US1/US2/US3:客户开票 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 客户申请开票。校验 → 金额拆分 → 调 ezPay 即时开立 → 落库。
|
|
|
+ * B2C/B2B/载具由 {@link #buildIssueData} 按 {@code category}/{@code carrierType} 分支处理。
|
|
|
+ */
|
|
|
+ public PosOrderInvoiceVo applyInvoice(ApplyInvoiceDto dto, Long currentUserId) {
|
|
|
+ Long orderId = dto.getOrderId();
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+ if (!Objects.equals(order.getUserId(), currentUserId)) {
|
|
|
+ throw new ServiceException("无权操作该订单");
|
|
|
+ }
|
|
|
+ if (order.getState() == null || order.getState() != ORDER_STATE_DONE) {
|
|
|
+ throw new ServiceException("订单未完成,暂不可开票");
|
|
|
+ }
|
|
|
+ if (order.getPayStatus() == null || order.getPayStatus() != ORDER_PAID) {
|
|
|
+ throw new ServiceException("订单未支付,暂不可开票");
|
|
|
+ }
|
|
|
+ Long storeId = order.getMdId();
|
|
|
+ PosStoreEzpay ez = assertInvoiceable(storeId);
|
|
|
+
|
|
|
+ // 场景入参校验(B2B 统编 / B2C 邮箱 / 载具号码)
|
|
|
+ validateInvoiceInput(dto);
|
|
|
+
|
|
|
+ // 防重复开票
|
|
|
+ PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
|
|
|
+ if (row != null && Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
|
|
|
+ throw new ServiceException("该订单已开票,不可重复开票");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 金额拆分(不含运费:invoiceTotal = amount - freight)
|
|
|
+ int amount = order.getAmount() == null ? 0 : order.getAmount();
|
|
|
+ int freight = (int) Math.round(order.getFreight() == null ? 0d : order.getFreight());
|
|
|
+ int invoiceTotal = amount - freight;
|
|
|
+ if (invoiceTotal <= 0) {
|
|
|
+ throw new ServiceException("订单可开票金额异常,无法开票");
|
|
|
+ }
|
|
|
+ int sales = (int) Math.round(invoiceTotal / 1.05);
|
|
|
+ int tax = invoiceTotal - sales;
|
|
|
+
|
|
|
+ // 组装 ezPay issue 参数 + 调用
|
|
|
+ Map<String, Object> inv = buildIssueData(dto, order, invoiceTotal, sales, tax);
|
|
|
+ EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
|
|
|
+
|
|
|
+ int newStatus;
|
|
|
+ String invoiceNumber = null, randomNum = null, invoiceUrl = null, failReason = null;
|
|
|
+ try {
|
|
|
+ JSONObject resp = ezPay.issueInvoice(ezpayBaseUrl, cfg, inv); String status = resp == null ? "" : resp.getString("Status");
|
|
|
+ JSONObject result = resp == null ? null : resp.getJSONObject("Result");
|
|
|
+ if ("SUCCESS".equals(status) && result != null && StrUtil.isNotBlank(result.getString("InvoiceNumber"))) {
|
|
|
+ invoiceNumber = result.getString("InvoiceNumber");
|
|
|
+ randomNum = result.getString("RandomNum");
|
|
|
+ invoiceUrl = result.getString("InvoiceTransNo");
|
|
|
+ newStatus = STATUS_ISSUED;
|
|
|
+ } else {
|
|
|
+ failReason = resp == null ? "ezPay 无回应" : (status + ":" + resp.getString("Message"));
|
|
|
+ newStatus = STATUS_FAILED;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ failReason = "ezPay 服务暂不可用:" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
|
|
|
+ newStatus = STATUS_FAILED;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 落库(uk_order_id 兜底防并发重复)
|
|
|
+ Date now = new Date();
|
|
|
+ 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.setTotalAmt(invoiceTotal);
|
|
|
+ saveRow.setSalesAmt(sales);
|
|
|
+ saveRow.setTaxAmt(tax);
|
|
|
+ saveRow.setApplyTime(now);
|
|
|
+ if (newStatus == STATUS_ISSUED) {
|
|
|
+ saveRow.setInvoiceNumber(invoiceNumber);
|
|
|
+ saveRow.setRandomNum(randomNum);
|
|
|
+ saveRow.setInvoiceUrl(invoiceUrl);
|
|
|
+ saveRow.setIssueTime(now);
|
|
|
+ saveRow.setFailReason(null);
|
|
|
+ } else {
|
|
|
+ saveRow.setFailReason(StrUtil.sub(failReason, 0, 480));
|
|
|
+ }
|
|
|
+ saveRow.setInvoiceStatus(newStatus);
|
|
|
+ try {
|
|
|
+ saveOrUpdate(saveRow);
|
|
|
+ } catch (DuplicateKeyException dup) {
|
|
|
+ throw new ServiceException("请勿重复提交开票申请");
|
|
|
+ }
|
|
|
+
|
|
|
+ return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 客户查询订单发票;门店不可开票时 invoiceStatus=null(客户端据此隐藏入口)。 */
|
|
|
+ public PosOrderInvoiceVo getInvoice(Long orderId, Long currentUserId) {
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ if (order == null || !Objects.equals(order.getUserId(), currentUserId)) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+ boolean can = canInvoice(order.getMdId());
|
|
|
+ PosOrderInvoiceVo vo = posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
+ if (vo == null) {
|
|
|
+ vo = new PosOrderInvoiceVo();
|
|
|
+ vo.setOrderId(orderId);
|
|
|
+ vo.setOrderNo(order.getDdId());
|
|
|
+ vo.setInvoiceStatus(can ? STATUS_NOT_ISSUED : null);
|
|
|
+ } else if (!can) {
|
|
|
+ vo.setInvoiceStatus(null);
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 商家端查询订单发票(校验商家为该订单门店归属人)。 */
|
|
|
+ public PosOrderInvoiceVo getInvoiceForMerchant(Long orderId, Long merchantUserId) {
|
|
|
+ requireOwnedOrder(orderId, merchantUserId);
|
|
|
+ return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 商家端作废发票(校验归属后复用作废逻辑)。 */
|
|
|
+ public int invalidForMerchant(Long orderId, String reason, Long merchantUserId) {
|
|
|
+ requireOwnedOrder(orderId, merchantUserId);
|
|
|
+ return invalid(orderId, reason);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 校验订单存在且属于该商家门店,返回订单;否则抛异常。 */
|
|
|
+ private PosOrder requireOwnedOrder(Long orderId, Long merchantUserId) {
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+ InfoUser user = infoUserService.getOne(
|
|
|
+ new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, merchantUserId));
|
|
|
+ if (user == null) {
|
|
|
+ throw new ServiceException("用户不存在");
|
|
|
+ }
|
|
|
+ String ut = user.getUserType();
|
|
|
+ boolean own;
|
|
|
+ if ("1".equals(ut) || "3".equals(ut)) {
|
|
|
+ // 普通/夜市商家:订单 shId 即商家 userId
|
|
|
+ own = merchantUserId.equals(order.getShId());
|
|
|
+ } else {
|
|
|
+ // 摊位商家:订单门店 mdId 即商家 storeId
|
|
|
+ own = user.getStoreId() != null && user.getStoreId().equals(order.getMdId());
|
|
|
+ }
|
|
|
+ if (!own) {
|
|
|
+ throw new ServiceException("无权操作该订单发票");
|
|
|
+ }
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== US4/US5:管理端 ====================
|
|
|
+
|
|
|
+ /** 管理端:订单发票列表(配合 PageHelper 分页)。 */
|
|
|
+ public List<PosOrderInvoiceVo> list(PosOrderInvoiceVo query) {
|
|
|
+ if (query != null && query.getDateRange() != null && query.getDateRange().length == 2) {
|
|
|
+ query.setCretimStart(query.getDateRange()[0]);
|
|
|
+ query.setCretimEnd(query.getDateRange()[1]);
|
|
|
+ }
|
|
|
+ return posOrderInvoiceMapper.selectInvoiceList(query);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 管理端:订单发票详情。 */
|
|
|
+ public PosOrderInvoiceVo detail(Long orderId) {
|
|
|
+ return posOrderInvoiceMapper.selectInvoiceDetail(orderId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 管理端:重新开票(仅 失败/作废 单;运营身份不校验客户归属)。 */
|
|
|
+ public int retry(Long orderId) {
|
|
|
+ PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
|
|
|
+ if (row == null) {
|
|
|
+ throw new ServiceException("发票记录不存在");
|
|
|
+ }
|
|
|
+ int st = row.getInvoiceStatus() == null ? STATUS_NOT_ISSUED : row.getInvoiceStatus();
|
|
|
+ if (st != STATUS_FAILED && st != STATUS_INVALID) {
|
|
|
+ throw new ServiceException("仅失败或作废的发票可重新开票");
|
|
|
+ }
|
|
|
+ // 重置为未开,复用客户开票参数重开(运营身份 currentUserId 传 null 跳过归属校验)
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+ PosStoreEzpay ez = assertInvoiceable(order.getMdId());
|
|
|
+ ApplyInvoiceDto dto = new ApplyInvoiceDto();
|
|
|
+ dto.setOrderId(orderId);
|
|
|
+ dto.setCategory(row.getInvoiceCategory());
|
|
|
+ dto.setBuyerName(row.getBuyerName());
|
|
|
+ dto.setBuyerUbn(row.getBuyerUbn());
|
|
|
+ dto.setBuyerEmail(row.getBuyerEmail());
|
|
|
+ dto.setCarrierType(row.getCarrierType());
|
|
|
+ dto.setCarrierNum(row.getCarrierNum());
|
|
|
+ // 复用核心开票(不含归属校验):直接走 issue + 落库
|
|
|
+ return reIssueAndSave(order, row, dto, ez);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 管理端:作废发票(仅 已开 单,调 ezPay invoice_invalid)。 */
|
|
|
+ public int invalid(Long orderId, String reason) {
|
|
|
+ PosOrderInvoice row = posOrderInvoiceMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosOrderInvoice>().eq(PosOrderInvoice::getOrderId, orderId));
|
|
|
+ if (row == null || !Integer.valueOf(STATUS_ISSUED).equals(row.getInvoiceStatus())) {
|
|
|
+ throw new ServiceException("仅已开发票可作废");
|
|
|
+ }
|
|
|
+ PosOrder order = posOrderMapper.selectPosOrderById(orderId);
|
|
|
+ PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, order.getMdId()));
|
|
|
+ if (ez == null) {
|
|
|
+ throw new ServiceException("门店 ezPay 凭证缺失,无法作废");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> postData = new LinkedHashMap<>();
|
|
|
+ postData.put("RespondType", "JSON");
|
|
|
+ postData.put("Version", "1.0");
|
|
|
+ postData.put("InvoiceNumber", row.getInvoiceNumber());
|
|
|
+ postData.put("InvalidReason", StrUtil.isBlank(reason) ? "商家作废" : reason);
|
|
|
+
|
|
|
+ try {
|
|
|
+ EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
|
|
|
+ JSONObject resp = ezPay.doPost(ezpayBaseUrl + EzPay.URL_INVALID, cfg, postData); String status = resp == null ? "" : resp.getString("Status");
|
|
|
+ if ("SUCCESS".equals(status)) {
|
|
|
+ row.setInvoiceStatus(STATUS_INVALID);
|
|
|
+ row.setInvalidTime(new Date());
|
|
|
+ saveOrUpdate(row);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ throw new ServiceException("作废失败:" + (resp == null ? "ezPay 无回应" : resp.getString("Message")));
|
|
|
+ } catch (ServiceException se) {
|
|
|
+ throw se;
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ServiceException("ezPay 服务暂不可用:" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部辅助 ====================
|
|
|
+
|
|
|
+ /** 复用开票核心(运营重开,无归属校验)。 */
|
|
|
+ private int reIssueAndSave(PosOrder order, PosOrderInvoice row, ApplyInvoiceDto dto, PosStoreEzpay ez) {
|
|
|
+ int amount = order.getAmount() == null ? 0 : order.getAmount();
|
|
|
+ int freight = (int) Math.round(order.getFreight() == null ? 0d : order.getFreight());
|
|
|
+ int invoiceTotal = amount - freight;
|
|
|
+ int sales = (int) Math.round(invoiceTotal / 1.05);
|
|
|
+ int tax = invoiceTotal - sales;
|
|
|
+
|
|
|
+ Map<String, Object> inv = buildIssueData(dto, order, invoiceTotal, sales, tax);
|
|
|
+ EzPayConfig cfg = new EzPayConfig(ez.getMerchantId(), ez.getHashKey(), ez.getHashIv());
|
|
|
+
|
|
|
+ int newStatus;
|
|
|
+ String invoiceNumber = null, randomNum = null, invoiceUrl = null, failReason = null;
|
|
|
+ try {
|
|
|
+ JSONObject resp = ezPay.issueInvoice(ezpayBaseUrl, cfg, inv);
|
|
|
+ String status = resp == null ? "" : resp.getString("Status");
|
|
|
+ JSONObject result = resp == null ? null : resp.getJSONObject("Result");
|
|
|
+ if ("SUCCESS".equals(status) && result != null && StrUtil.isNotBlank(result.getString("InvoiceNumber"))) {
|
|
|
+ invoiceNumber = result.getString("InvoiceNumber");
|
|
|
+ randomNum = result.getString("RandomNum");
|
|
|
+ invoiceUrl = result.getString("InvoiceTransNo");
|
|
|
+ newStatus = STATUS_ISSUED;
|
|
|
+ } else {
|
|
|
+ failReason = resp == null ? "ezPay 无回应" : (status + ":" + resp.getString("Message"));
|
|
|
+ newStatus = STATUS_FAILED;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ failReason = "ezPay 服务暂不可用:" + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
|
|
|
+ newStatus = STATUS_FAILED;
|
|
|
+ }
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ if (newStatus == STATUS_ISSUED) {
|
|
|
+ row.setInvoiceNumber(invoiceNumber);
|
|
|
+ row.setRandomNum(randomNum);
|
|
|
+ row.setInvoiceUrl(invoiceUrl);
|
|
|
+ row.setIssueTime(now);
|
|
|
+ row.setFailReason(null);
|
|
|
+ } else {
|
|
|
+ row.setFailReason(StrUtil.sub(failReason, 0, 480));
|
|
|
+ }
|
|
|
+ row.setInvoiceStatus(newStatus);
|
|
|
+ saveOrUpdate(row);
|
|
|
+ return newStatus == STATUS_ISSUED ? 1 : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 校验门店可开票并返回凭证;不可开票抛异常。 */
|
|
|
+ private PosStoreEzpay assertInvoiceable(Long storeId) {
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new ServiceException("订单未关联门店,暂不可开票");
|
|
|
+ }
|
|
|
+ PosStore store = posStoreMapper.selectPosStoreById(storeId);
|
|
|
+ if (store != null && Integer.valueOf(1).equals(store.getInvoiceExempt())) {
|
|
|
+ throw new ServiceException("该门店免用发票,无需开票");
|
|
|
+ }
|
|
|
+ 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())) {
|
|
|
+ throw new ServiceException("该门店暂不支持电子发票");
|
|
|
+ }
|
|
|
+ return ez;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 门店是否可开票(不抛异常)。 */
|
|
|
+ private boolean canInvoice(Long storeId) {
|
|
|
+ if (storeId == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ PosStore store = posStoreMapper.selectPosStoreById(storeId);
|
|
|
+ if (store != null && Integer.valueOf(1).equals(store.getInvoiceExempt())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ PosStoreEzpay ez = posStoreEzpayMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<PosStoreEzpay>().eq(PosStoreEzpay::getStoreId, storeId));
|
|
|
+ return ez != null && Integer.valueOf(2).equals(ez.getEzpayStatus())
|
|
|
+ && Integer.valueOf(1).equals(ez.getIsEnabled());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 场景入参校验(service 内强校验,拦截非法请求不调 ezPay)。 */
|
|
|
+ private void validateInvoiceInput(ApplyInvoiceDto dto) {
|
|
|
+ if ("B2B".equals(dto.getCategory())) {
|
|
|
+ if (StrUtil.isBlank(dto.getBuyerUbn()) || !dto.getBuyerUbn().matches("\\d{8}")) {
|
|
|
+ throw new ServiceException("统编格式不正确,须为 8 位数字");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ("B2C".equals(dto.getCategory()) && StrUtil.isBlank(dto.getCarrierType()) && StrUtil.isBlank(dto.getBuyerEmail())) {
|
|
|
+ throw new ServiceException("接收邮箱不能为空");
|
|
|
+ }
|
|
|
+ if (StrUtil.isNotBlank(dto.getCarrierType()) && StrUtil.isBlank(dto.getCarrierNum())) {
|
|
|
+ throw new ServiceException("载具号码不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 组装 ezPay invoice_issue 业务参数(Category/PrintFlag/Buyer/Amt/明细)。 */
|
|
|
+ private Map<String, Object> buildIssueData(ApplyInvoiceDto dto, PosOrder order,
|
|
|
+ int invoiceTotal, int sales, int tax) {
|
|
|
+ Map<String, Object> inv = new LinkedHashMap<>();
|
|
|
+ inv.put("MerchantOrderNo", order.getDdId());
|
|
|
+ inv.put("Category", dto.getCategory());
|
|
|
+ inv.put("BuyerName", dto.getBuyerName());
|
|
|
+ if ("B2B".equals(dto.getCategory())) {
|
|
|
+ inv.put("BuyerUBN", dto.getBuyerUbn());
|
|
|
+ inv.put("PrintFlag", "Y");
|
|
|
+ } else if (StrUtil.isNotBlank(dto.getCarrierType())) {
|
|
|
+ inv.put("CarrierType", dto.getCarrierType());
|
|
|
+ inv.put("CarrierNum", dto.getCarrierNum());
|
|
|
+ inv.put("PrintFlag", "N");
|
|
|
+ } else {
|
|
|
+ inv.put("BuyerEmail", dto.getBuyerEmail());
|
|
|
+ inv.put("PrintFlag", "Y");
|
|
|
+ }
|
|
|
+ inv.put("TaxType", "1");
|
|
|
+ inv.put("TaxRate", "5");
|
|
|
+ inv.put("Amt", String.valueOf(sales));
|
|
|
+ inv.put("TaxAmt", String.valueOf(tax));
|
|
|
+ inv.put("TotalAmt", String.valueOf(invoiceTotal));
|
|
|
+ appendItems(inv, order.getFood(), invoiceTotal);
|
|
|
+ return inv;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 逐商品明细(research D2/D3):从 food JSON 解析,优惠按比例分摊到单价(方案 B)。
|
|
|
+ * 每行满足 ItemCount × ItemPrice = ItemAmt(单价决定金额,ezPay 逐商品校验通过)。
|
|
|
+ */
|
|
|
+ private void appendItems(Map<String, Object> inv, String foodJson, int invoiceTotal) {
|
|
|
+ JSONArray arr = StrUtil.isBlank(foodJson) ? null : com.alibaba.fastjson.JSON.parseArray(foodJson);
|
|
|
+ if (arr == null || arr.isEmpty()) {
|
|
|
+ inv.put("ItemName", "餐点");
|
|
|
+ inv.put("ItemCount", "1");
|
|
|
+ inv.put("ItemUnit", "份");
|
|
|
+ inv.put("ItemPrice", String.valueOf(invoiceTotal));
|
|
|
+ inv.put("ItemAmt", String.valueOf(invoiceTotal));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ int foodTotal = foodOriginalTotal(arr);
|
|
|
+ double ratio = foodTotal > 0 ? (double) invoiceTotal / foodTotal : 1.0;
|
|
|
+ StringBuilder name = new StringBuilder(), count = new StringBuilder(),
|
|
|
+ unit = new StringBuilder(), price = new StringBuilder(), amt = new StringBuilder();
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ com.alibaba.fastjson.JSONObject it = arr.getJSONObject(i);
|
|
|
+ int p = it.getIntValue("price") + it.getIntValue("otherPrice");
|
|
|
+ int c = it.getIntValue("number");
|
|
|
+ if (c <= 0) {
|
|
|
+ c = 1;
|
|
|
+ }
|
|
|
+ int itemPrice = (int) Math.round(p * ratio);
|
|
|
+ int itemAmt = itemPrice * c;
|
|
|
+ if (i > 0) {
|
|
|
+ name.append("|"); count.append("|"); unit.append("|"); price.append("|"); amt.append("|");
|
|
|
+ }
|
|
|
+ String nm = it.getString("name");
|
|
|
+ name.append(StrUtil.isBlank(nm) ? "餐点" : nm);
|
|
|
+ count.append(c);
|
|
|
+ unit.append("個");
|
|
|
+ price.append(itemPrice);
|
|
|
+ amt.append(itemAmt);
|
|
|
+ }
|
|
|
+ inv.put("ItemName", name.toString());
|
|
|
+ inv.put("ItemCount", count.toString());
|
|
|
+ inv.put("ItemUnit", unit.toString());
|
|
|
+ inv.put("ItemPrice", price.toString());
|
|
|
+ inv.put("ItemAmt", amt.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** food 商品原价总额(含税)= Σ(price + otherPrice) × number。 */
|
|
|
+ private int foodOriginalTotal(JSONArray arr) {
|
|
|
+ int sum = 0;
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ com.alibaba.fastjson.JSONObject it = arr.getJSONObject(i);
|
|
|
+ int p = it.getIntValue("price") + it.getIntValue("otherPrice");
|
|
|
+ int c = it.getIntValue("number");
|
|
|
+ sum += p * (c <= 0 ? 1 : c);
|
|
|
+ }
|
|
|
+ return sum;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 新增或更新(显式设置时间与人)。 */
|
|
|
+ private void saveOrUpdate(PosOrderInvoice row) {
|
|
|
+ Date now = new Date();
|
|
|
+ String user = currentUser();
|
|
|
+ if (row.getId() == null) {
|
|
|
+ row.setCreateTime(now);
|
|
|
+ row.setUpdateTime(now);
|
|
|
+ row.setCreateBy(user);
|
|
|
+ row.setUpdateBy(user);
|
|
|
+ posOrderInvoiceMapper.insert(row);
|
|
|
+ } else {
|
|
|
+ row.setUpdateTime(now);
|
|
|
+ row.setUpdateBy(user);
|
|
|
+ posOrderInvoiceMapper.updateById(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String currentUser() {
|
|
|
+ try {
|
|
|
+ return SecurityUtils.getUsername();
|
|
|
+ } catch (Exception e) {
|
|
|
+ return "system";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|