Parcourir la source

订单列表和详情返回发票可打印状态及申请次数

qmj il y a 1 jour
Parent
commit
43f69b7695

+ 3 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java

@@ -177,6 +177,8 @@ public class PosOrderController extends BaseController {
     @Autowired
     private OrderInvoiceService orderInvoiceService;
     @Autowired
+    private com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService invoicePrintAvailability;
+    @Autowired
     private OrderLifecycleService orderLifecycleService;
     @Autowired
     private LinePayService linePayService;
@@ -743,6 +745,7 @@ public class PosOrderController extends BaseController {
         queryWrapper.eq("dd_id", ddid);
         PosOrder orlist = posOrderService.getOne(queryWrapper);
         requireOrderDetailAccess(orlist);
+        invoicePrintAvailability.fill(java.util.Collections.singletonList(orlist));
         // 自动序列化所有PosOrder字段,新增字段无需手动添加
         JSONObject org = JSON.parseObject(
             JSON.toJSONStringWithDateFormat(orlist, "yyyy-MM-dd HH:mm:ss")

+ 3 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -78,6 +78,8 @@ public class PosOrderShOprateController extends BaseController {
     @Autowired
     private OrderInvoiceService orderInvoiceService;
     @Autowired
+    private com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService invoicePrintAvailability;
+    @Autowired
     private LinePayOrderGuard linePayOrderGuard;
     @Autowired
     private LinePayRefundService linePayRefundService;
@@ -978,6 +980,7 @@ public class PosOrderShOprateController extends BaseController {
                 new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
         com.baomidou.mybatisplus.extension.plugins.pagination.Page<PosOrder> result = posOrderService.page(pageParam, wrapper);
 
+        invoicePrintAvailability.fill(result.getRecords());
         return success(result);
     }
 

+ 37 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/order/invoice/OrderInvoicePrintAvailabilityService.java

@@ -0,0 +1,37 @@
+package com.ruoyi.app.order.invoice;
+
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.mapper.PosOrderInvoicePrintAssetMapper;
+import org.springframework.stereotype.Service;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/** 只为已完成权限筛选的订单补充打印入口信息,列表批量查询避免逐单读取文件。 */
+@Service
+public class OrderInvoicePrintAvailabilityService {
+    private final PosOrderInvoicePrintAssetMapper assets;
+    public OrderInvoicePrintAvailabilityService(PosOrderInvoicePrintAssetMapper assets) { this.assets = assets; }
+
+    public void fill(List<PosOrder> orders) {
+        if (orders == null || orders.isEmpty()) return;
+        var ids = orders.stream().filter(Objects::nonNull).map(PosOrder::getId)
+                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        orders.stream().filter(Objects::nonNull).forEach(order -> {
+            order.setCanPrintInvoice(false); order.setInvoicePrintCount(0);
+        });
+        if (ids.isEmpty()) return;
+        var rows = assets.selectOrderAvailability(ids, InvoicePrintAssetService.VERSION);
+        var byOrder = rows.stream().collect(Collectors.toMap(
+                com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability::getOrderId, Function.identity()));
+        for (PosOrder order : orders) {
+            if (order == null) continue;
+            var row = byOrder.get(order.getId());
+            if (row != null) {
+                order.setCanPrintInvoice(Boolean.TRUE.equals(row.getCanPrintInvoice()));
+                order.setInvoicePrintCount(row.getInvoicePrintCount());
+            }
+        }
+    }
+}

+ 10 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderImGroupControllerTest.java

@@ -26,6 +26,7 @@ class OrderImGroupControllerTest {
     private final MerchantStoreAccessService merchants = mock(MerchantStoreAccessService.class);
 
     OrderImGroupControllerTest() {
+        ReflectionTestUtils.setField(controller, "invoicePrintAvailability", mock(com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService.class));
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", merchants);
@@ -134,9 +135,18 @@ class OrderImGroupControllerTest {
             when(promotions.selectByOrderId(7L)).thenReturn(java.util.List.of());
             ReflectionTestUtils.setField(controller, "posOrderPromotionMapper", promotions);
 
+            var printMapper = mock(com.ruoyi.system.mapper.PosOrderInvoicePrintAssetMapper.class);
+            var availability = new com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability();
+            availability.setOrderId(7L); availability.setCanPrintInvoice(true); availability.setInvoicePrintCount(1);
+            when(printMapper.selectOrderAvailability(any(), anyString())).thenReturn(java.util.List.of(availability));
+            ReflectionTestUtils.setField(controller, "invoicePrintAvailability",
+                    new com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService(printMapper));
+
             var data = (com.alibaba.fastjson.JSONObject) controller.getorderxq("token", "ORDER-1").get("data");
 
             assertEquals("detail-room", data.getString("imGroupId"));
+            assertTrue(data.getBooleanValue("canPrintInvoice"));
+            assertEquals(1, data.getIntValue("invoicePrintCount"));
             order.setImGroupId(null);
             var unbound = (com.alibaba.fastjson.JSONObject) controller.getorderxq("token", "ORDER-1").get("data");
             assertTrue(unbound.containsKey("imGroupId"));

+ 24 - 3
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderListPaymentFilterTest.java

@@ -55,6 +55,7 @@ class OrderListPaymentFilterTest {
         IPosOrderService orders = mock(IPosOrderService.class);
         IInfoUserService users = mock(IInfoUserService.class);
         PosOrderShOprateController controller = new PosOrderShOprateController();
+        ReflectionTestUtils.setField(controller, "invoicePrintAvailability", mock(com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService.class));
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
@@ -65,9 +66,27 @@ class OrderListPaymentFilterTest {
         merchant.setUserId(801L);
         merchant.setUserType("1");
         when(users.getOne(any())).thenReturn(merchant);
-        when(orders.page(any(Page.class), any(Wrapper.class))).thenReturn(new Page<>());
-
-        controller.orderList(tokenFor(801L), 1, 10, "pending", null, null);
+        PosOrder order = new PosOrder();
+        order.setId(7L);
+        Page<PosOrder> page = new Page<>();
+        page.setRecords(Collections.singletonList(order));
+        when(orders.page(any(Page.class), any(Wrapper.class))).thenReturn(page);
+        var assets = mock(com.ruoyi.system.mapper.PosOrderInvoicePrintAssetMapper.class);
+        var availability = new com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability();
+        availability.setOrderId(7L);
+        availability.setCanPrintInvoice(true);
+        availability.setInvoicePrintCount(0);
+        when(assets.selectOrderAvailability(Collections.singletonList(7L),
+                com.ruoyi.app.order.invoice.InvoicePrintAssetService.VERSION))
+                .thenReturn(Collections.singletonList(availability));
+        ReflectionTestUtils.setField(controller, "invoicePrintAvailability",
+                new com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService(assets));
+
+        var response = controller.orderList(tokenFor(801L), 1, 10, "pending", null, null);
+        var json = com.alibaba.fastjson2.JSON.parseObject(com.alibaba.fastjson2.JSON.toJSONString(response));
+        var item = json.getJSONObject("data").getJSONArray("records").getJSONObject(0);
+        assertTrue(item.getBooleanValue("canPrintInvoice"));
+        assertEquals(0, item.getInteger("invoicePrintCount"));
 
         LambdaQueryWrapper<PosOrder> query = capturedQuery(orders);
         String sql = query.getSqlSegment();
@@ -86,6 +105,7 @@ class OrderListPaymentFilterTest {
         IPosOrderService orders = mock(IPosOrderService.class);
         IInfoUserService users = mock(IInfoUserService.class);
         PosOrderShOprateController controller = new PosOrderShOprateController();
+        ReflectionTestUtils.setField(controller, "invoicePrintAvailability", mock(com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService.class));
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         InfoUser merchant = new InfoUser();
@@ -127,6 +147,7 @@ class OrderListPaymentFilterTest {
         IInfoUserService users = mock(IInfoUserService.class);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
         PosOrderShOprateController controller = new PosOrderShOprateController();
+        ReflectionTestUtils.setField(controller, "invoicePrintAvailability", mock(com.ruoyi.app.order.invoice.OrderInvoicePrintAvailabilityService.class));
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);

+ 37 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/order/invoice/OrderInvoicePrintAvailabilityServiceTest.java

@@ -0,0 +1,37 @@
+package com.ruoyi.app.order.invoice;
+
+import com.ruoyi.system.domain.PosOrder;
+import com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability;
+import com.ruoyi.system.mapper.PosOrderInvoicePrintAssetMapper;
+import org.junit.jupiter.api.Test;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class OrderInvoicePrintAvailabilityServiceTest {
+    @Test void fillsCurrentPageOnceAndDefaultsOrdersWithoutInvoices() {
+        var mapper = mock(PosOrderInvoicePrintAssetMapper.class);
+        var service = new OrderInvoicePrintAvailabilityService(mapper);
+        PosOrder first = order(1), reprint = order(2), pending = order(3), absent = order(4);
+        when(mapper.selectOrderAvailability(List.of(1L,2L,3L,4L), InvoicePrintAssetService.VERSION))
+                .thenReturn(List.of(row(1,true,0),row(2,true,2),row(3,false,0)));
+        service.fill(List.of(first,reprint,pending,absent));
+        assertTrue(first.getCanPrintInvoice()); assertEquals(0,first.getInvoicePrintCount());
+        assertTrue(reprint.getCanPrintInvoice()); assertEquals(2,reprint.getInvoicePrintCount());
+        assertFalse(pending.getCanPrintInvoice()); assertFalse(absent.getCanPrintInvoice());
+        assertEquals(0,absent.getInvoicePrintCount());
+        verify(mapper).selectOrderAvailability(List.of(1L,2L,3L,4L), InvoicePrintAssetService.VERSION);
+        verifyNoMoreInteractions(mapper);
+    }
+    @Test void emptyPagesNeverQueryDatabase() {
+        var mapper = mock(PosOrderInvoicePrintAssetMapper.class);
+        var service = new OrderInvoicePrintAvailabilityService(mapper);
+        service.fill(List.of()); service.fill(null);
+        verifyNoInteractions(mapper);
+    }
+    private PosOrder order(long id) { var o = new PosOrder(); o.setId(id); return o; }
+    private OrderInvoicePrintAvailability row(long id, boolean ready, int count) {
+        var r = new OrderInvoicePrintAvailability(); r.setOrderId(id);
+        r.setCanPrintInvoice(ready); r.setInvoicePrintCount(count); return r;
+    }
+}

+ 8 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrder.java

@@ -40,6 +40,14 @@ public class PosOrder {
     @TableField("im_group_id")
     private String imGroupId;
 
+    /** 当前发票纸本文件已就绪,可发起打印/补印;提交打印时仍校验权限和供应商状态。 */
+    @TableField(exist = false)
+    private Boolean canPrintInvoice;
+
+    /** 当前发票在本系统的打印申请次数,重开后重新计数,不等同于实际出纸次数。 */
+    @TableField(exist = false)
+    private Integer invoicePrintCount;
+
     /**
      * 商家id
      */

+ 11 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/OrderInvoicePrintAvailability.java

@@ -0,0 +1,11 @@
+package com.ruoyi.system.domain.vo;
+
+import lombok.Data;
+
+/** 订单查询的轻量打印信息,不包含PDF或图像内容。 */
+@Data
+public class OrderInvoicePrintAvailability {
+    private Long orderId;
+    private Boolean canPrintInvoice;
+    private Integer invoicePrintCount;
+}

+ 2 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderInvoicePrintAssetMapper.java

@@ -10,5 +10,7 @@ public interface PosOrderInvoicePrintAssetMapper extends BaseMapper<PosOrderInvo
     PosOrderInvoicePrintAsset selectForInvoice(@Param("invoiceId") Long invoiceId,
             @Param("transNo") String transNo, @Param("version") String version);
     PosOrderInvoicePrintAsset selectCurrentReady(@Param("orderId") Long orderId, @Param("version") String version);
+    List<com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability> selectOrderAvailability(
+            @Param("orderIds") List<Long> orderIds, @Param("version") String version);
     List<Long> selectCandidates(@Param("now") Date now, @Param("version") String version, @Param("limit") int limit);
 }

+ 18 - 0
ruoyi-system/src/main/resources/mapper/chanting/PosOrderInvoicePrintAssetMapper.xml

@@ -16,6 +16,24 @@
             AND i.invoice_trans_no=a.invoice_trans_no AND i.invoice_number=a.invoice_number
         WHERE i.order_id=#{orderId} AND a.renderer_version=#{version} AND a.status='READY'
     </select>
+    <!-- 仅查当前页已授权订单;不读取PDF/图片大字段,不触发生成或联网查票。 -->
+    <select id="selectOrderAvailability" resultType="com.ruoyi.system.domain.vo.OrderInvoicePrintAvailability">
+        SELECT i.order_id AS orderId,
+            CASE WHEN i.invoice_status=1
+                AND COALESCE(i.carrier_type,'')='' AND COALESCE(i.carrier_num,'')=''
+                AND COALESCE(i.love_code,'')='' AND i.invoice_category IN ('B2C','B2B')
+                AND EXISTS (SELECT 1 FROM pos_order_invoice_print_asset a
+                    WHERE a.invoice_id=i.id AND a.order_id=i.order_id
+                      AND a.invoice_trans_no=i.invoice_trans_no AND a.invoice_number=i.invoice_number
+                      AND a.renderer_version=#{version} AND a.status='READY')
+                THEN 1 ELSE 0 END AS canPrintInvoice,
+            (SELECT COUNT(*) FROM pos_order_invoice_print_job j
+                WHERE j.invoice_id=i.id AND j.order_id=i.order_id
+                  AND j.invoice_trans_no=i.invoice_trans_no AND j.invoice_number=i.invoice_number) AS invoicePrintCount
+        FROM pos_order_invoice i
+        WHERE i.order_id IN
+        <foreach collection="orderIds" item="orderId" open="(" separator="," close=")">#{orderId}</foreach>
+    </select>
     <!-- 已提交的开票行即持久任务源:进程重启、开票回调重复均不会漏掉生成或消耗打印次数。 -->
     <select id="selectCandidates" resultType="java.lang.Long">
         SELECT i.order_id FROM pos_order_invoice i

+ 29 - 0
ruoyi-system/src/test/java/com/ruoyi/system/mapper/OrderInvoicePrintAvailabilityMapperTest.java

@@ -0,0 +1,29 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.junit.jupiter.api.Test;
+import java.util.List;
+import java.util.Map;
+import static org.junit.jupiter.api.Assertions.*;
+
+class OrderInvoicePrintAvailabilityMapperTest {
+    @Test void queryIsBoundedAndRejectsStaleOrIneligibleAssetsWithoutLoadingFiles() throws Exception {
+        var config = new MybatisConfiguration();
+        String resource = "mapper/chanting/PosOrderInvoicePrintAssetMapper.xml";
+        try (var input = getClass().getClassLoader().getResourceAsStream(resource)) {
+            new XMLMapperBuilder(input,config,resource,config.getSqlFragments()).parse();
+        }
+        var bound = config.getMappedStatement(PosOrderInvoicePrintAssetMapper.class.getName()+".selectOrderAvailability")
+                .getBoundSql(Map.of("orderIds",List.of(1L,2L),"version","VERSION"));
+        String sql = bound.getSql().replaceAll("\\s+"," ");
+        for (String guard : List.of("i.invoice_status=1", "a.invoice_trans_no=i.invoice_trans_no",
+                "a.invoice_number=i.invoice_number", "a.status='READY'", "a.renderer_version=?",
+                "COALESCE(i.carrier_type,'')=''", "COALESCE(i.carrier_num,'')=''",
+                "COALESCE(i.love_code,'')=''", "i.invoice_category IN ('B2C','B2B')", "i.order_id IN")) {
+            assertTrue(sql.contains(guard),guard);
+        }
+        assertFalse(sql.contains("original_pdf")); assertFalse(sql.contains("SELECT *"));
+        assertEquals(3,bound.getParameterMappings().size());
+    }
+}

+ 26 - 0
specs/010-order-invoice/frontend-app-printing.md

@@ -125,3 +125,29 @@
 ### 2026-11-02 字体修复说明
 
 后端字体改为完整 Noto Sans CJK TC(固定 Regular 字重),随 JAR 打包。支持简繁中文、英文、数字与常用标点混排,App 不安装字体、不转换文字,打印仍使用后端图片。历史“测试门店”缺字导致的失败文件,更新后端后会在原定重试时间重新生成;不需要重新开票,本次无新增 SQL。此前 Phase12/13 迁移仍须已执行。特殊 Emoji 等不保证覆盖,遇到不支持的字形保留服务端错误原因,不静默丢字。
+
+
+## 订单列表、详情的打印入口状态(2026-11-03)
+
+请求参数不变,以下位置新增字段:
+
+| 接口 | 返回位置 |
+| --- | --- |
+| `/system/orderShOprate/orderList` | `data.records[]` 每条订单 |
+| `/system/order/getorderxq` | `data` 订单详情 |
+
+```json
+{
+  "canPrintInvoice": true,
+  "invoicePrintCount": 0
+}
+```
+
+- `canPrintInvoice=false`:不展示可用的发票打印入口。包括无发票、未开立、开立失败、作废、载具、捐赠及文件未就绪。
+- `canPrintInvoice=true` 且 `invoicePrintCount=0`:显示“打印发票”,使用现有首次打印流程。
+- `canPrintInvoice=true` 且 `invoicePrintCount>0`:显示“申请补印”,沿用现有补印确认与参数要求,不能再以首次方式申请。
+- `invoicePrintCount` 是当前这张发票在本系统成功领取打印文件的申请次数,包含补印;不是打印机出纸次数。无记录为0,重开后按新发票计数。
+
+不要仅根据有发票号就显示可打印,也不要把 `canPrintInvoice` 当作最终打印授权。列表/详情只检查本地状态,不调用 ezPay;点击后仍须调用现有 `printInvoice`,处理状态变化、实时查票失败及补印确认错误。未就绪时后续刷新订单可获取最新状态。
+
+本轮不调整 App 请求参数,不增加 SQL;App 只需读取两个响应字段控制入口。载具转纸本另行实现。本字段针对电子发票证明联,与餐点小票打印无关。

+ 8 - 0
specs/010-order-invoice/plan.md

@@ -224,3 +224,11 @@ src/lang/{zh,tw,en,vi}.js                    # 四语言加 key
 4. 定向Invoice/小票测试、模块构建、JAR字体校验、只读真实订单3248验证和PNG视觉/条码回读;不部署、不写数据库。
 
 2026-11-02 字体修复验证:39项定向测试及模块package通过;新增缺字/cause回归先失败后通过。真实订单3248只读生成PDF与PNG成功,三种码回读一致;JAR校验新字体存在且旧子集已移除。没有迁移或数据写入;按用户要求提交推送,由用户发布。
+
+
+## 2026-11-03 订单打印状态补充计划
+
+1. 在 PosOrder 增加两个非持久化响应字段,默认不可打印、次数0。
+2. Mapper 批量按订单ID读取当前发票身份、当前版式 READY 文件及打印申请次数;服务统一回填,不读取 PDF/PNG。
+3. 商家列表在分页权限筛选后回填,通用详情在订单权限校验后回填,原请求与打印流程不变。
+4. 验证批量回填、无票默认值、空页、两接口真实序列化响应及 Mapper 查询范围/身份/资格条件;更新 App 文档。无新增SQL,不执行数据库迁移。

+ 9 - 0
specs/010-order-invoice/spec.md

@@ -265,3 +265,12 @@
 ## 2026-11-02 发票字体覆盖修复
 
 订单3248的真实ezPay查询已成功,旧Noto Sans TC繁体子集缺少U+6D4B(测),导致简体门店名无法生成交易明细及证明联。改用完整Noto Sans CJK TC字体,保留台湾字形风格,同时覆盖简繁中文、英文、数字、常用标点;不转写、不丢弃原始文本,不承诺所有Unicode/Emoji均有字形。字体仍作为classpath资源随JAR部署,不要求服务器安装字体。原件/补印版式和金额规则不变。保留渲染异常cause供服务端定位,不在业务响应中暴露异常细节。已生成成功的文件保持原快照;失败文件按既有重试时间重新生成,无需重新开票或迁移SQL。
+
+
+## 2026-11-03 订单返回现有发票打印状态
+
+- `/system/orderShOprate/orderList` 的每条订单和 `/system/order/getorderxq` 的订单详情新增 `canPrintInvoice`、`invoicePrintCount`,请求参数保持不变。
+- `canPrintInvoice=true` 表示本地当前发票已开立、属于现有可打印纸本类型,且当前开立身份、当前版式的预生成文件已就绪;首次与补印均可为 true。无发票、未开立、失败、作废、载具、捐赠或文件未就绪时为 false。
+- `invoicePrintCount` 仅统计当前开立身份的本系统打印申请;无记录返回0,不代表打印机实际成功出纸次数。重开发票不继承旧票计数。
+- 列表只批量查询当前已授权页的状态,不加载文件大字段、不生成文件、不调用第三方;详情在权限校验后补充。真正申请打印时仍执行现有鉴权、实时查票及补印确认。
+- 用户确认本轮只返回现有能力;载具转纸本另做,不变更现有打印资格、不修改 App、不新增数据库字段。

+ 10 - 0
specs/010-order-invoice/tasks.md

@@ -347,3 +347,13 @@ description: "Task list for 订单 ezPay 电子发票开立"
 只读真实订单3248:ezPay查询SUCCESS;交易明细PDF 11729字节、证明联PDF 20062字节、PNG 2段,Code39及左右二维码回读一致。没有更新数据库、创建打印申请、消耗次数或重新开票。新的静态字体21658028字节(21.7MB/20.7MiB),来源/校验和/固定字重过程记入字体README。
 
 本轮随代码提交推送,未部署;无需新增SQL。已存在的失败任务发布后按原重试时间恢复,不重置已有成功文件或打印资格;实机出纸仍属于T075外部验收。
+
+
+## 2026-11-03 订单打印入口状态
+
+- [x] T079 两个订单查询返回 canPrintInvoice / invoicePrintCount;授权后批量回填,限定当前开立身份和版式,不加载文件或联网。
+- [x] T080 补充批量回填、无票/空页默认值、列表与详情序列化及 Mapper 资格/身份/范围校验;更新规格、计划、App接入文档。
+
+验证:JDK21执行 `mvn -q -pl ruoyi-admin -am -Dtest=*Invoice*Test,OrderImGroupControllerTest,OrderListPaymentFilterTest -Dsurefire.failIfNoSpecifiedTests=false package`,49项测试通过,失败/错误/跳过均0,模块构建通过。日志 `ruoyi-admin/target/invoice-availability.log`。git diff --check通过。
+
+按用户确认仅返回现有能力,载具转纸本另做。无新增SQL、未修改App、未部署;未执行全项目回归、真实MySQL/ezPay或App/打印机联调,T075外部验收继续保留。按用户要求在 test-202609v2 提交推送。