qmj před 3 dny
rodič
revize
d0ce1ea4e8

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 3 - 0
.claude/homunculus/observations.jsonl


+ 5 - 4
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderInvoiceService.java

@@ -404,9 +404,7 @@ public class OrderInvoiceService {
                 throw new ServiceException("统编格式不正确,须为 8 位数字");
             }
         }
-        if ("B2C".equals(dto.getCategory()) && StrUtil.isBlank(dto.getCarrierType()) && StrUtil.isBlank(dto.getBuyerEmail())) {
-            throw new ServiceException("接收邮箱不能为空");
-        }
+        // B2C 无载具时邮箱可选(PrintFlag=Y,客户在系统查看发票);载具号码随 carrierType 必填
         if (StrUtil.isNotBlank(dto.getCarrierType()) && StrUtil.isBlank(dto.getCarrierNum())) {
             throw new ServiceException("载具号码不能为空");
         }
@@ -427,7 +425,10 @@ public class OrderInvoiceService {
             inv.put("CarrierNum", dto.getCarrierNum());
             inv.put("PrintFlag", "N");
         } else {
-            inv.put("BuyerEmail", dto.getBuyerEmail());
+            // B2C 无载具:有邮箱则发送,无邮箱则不传(PrintFlag=Y,客户在系统查看)
+            if (StrUtil.isNotBlank(dto.getBuyerEmail())) {
+                inv.put("BuyerEmail", dto.getBuyerEmail());
+            }
             inv.put("PrintFlag", "Y");
         }
         inv.put("TaxType", "1");

+ 1 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ApplyInvoiceDto.java

@@ -32,7 +32,7 @@ public class ApplyInvoiceDto {
     @Pattern(regexp = "\\d{8}", message = "统一编号必须为 8 位数字")
     private String buyerUbn;
 
-    /** 接收邮箱(B2C 无载具时必填,service 内校验) */
+    /** 接收邮箱(B2C 无载具时可选:填了发邮件,不填则客户在系统查看发票) */
     @Email(message = "邮箱格式不正确")
     private String buyerEmail;
 

+ 49 - 9
specs/010-order-invoice/contracts/api.md

@@ -12,15 +12,55 @@
 客户对已完成订单申请开票。
 
 **请求体** `ApplyInvoiceDto`:
-| 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| orderId | Long | 是 | 订单 id(须为当前登录客户的订单、state=3、payStatus=1) |
-| category | String | 是 | `B2C` / `B2B` |
-| buyerName | String | 是 | 买方名称(B2C 客户名 / B2B 公司名) |
-| buyerUbn | String | B2B 必填 | 统一编号 8 码(B2B 必填且校验格式) |
-| buyerEmail | String | B2C 无载具时必填 | 接收邮箱 |
-| carrierType | String | 否 | `0`手机条码 / `1`自然人凭证 / `2`ezPay会员载具 |
-| carrierNum | String | carrierType 非空时必填 | 载具号码 |
+
+**① 字段含义**
+
+| 字段 | 类型 | 含义 | 格式 / 示例 |
+|------|------|------|------------|
+| orderId | Long | 要开票的订单 id | 必须是当前登录客户自己的订单,且已完成 `state=3`、已支付 `payStatus=1` |
+| category | String | 发票类型 | `B2C`=个人发票(开给个人);`B2B`=公司发票(开给营业人,凭统编报账) |
+| buyerName | String | 买方名称 | B2C 填个人姓名;B2B 填公司全名 |
+| buyerUbn | String | 买方统一编号(统编) | 8 位数字,如 `12345678`。**仅 B2B 用** |
+| buyerEmail | String | 接收发票的邮箱 | 可选。B2C 填了则发邮件通知,不填则客户在系统查看 |
+| carrierType | String | 电子载具类型 | `0`=手机条码 / `1`=自然人凭证 / `2`=ezPay 会员载具;**留空 = 不用载具** |
+| carrierNum | String | 载具号码 | 随 carrierType:手机条码以 `/` 开头(`/ABC1234`);自然人凭证 `2字母+14数字`;ezPay 会员载具填会员账号 |
+
+**② 什么时候必填(按场景)**
+
+| 场景 | buyerUbn | buyerEmail | carrierType + carrierNum |
+|------|----------|------------|--------------------------|
+| **B2B 公司发票** | ✅ 必填(8 位数字) | ⚪ 可不填(B2B 默认打印,填了不生效) | ⚪ 可不填(B2B 不走载具) |
+| **B2C 个人 / 邮箱接收** | ❌ 不填 | ✅ 填(发邮件) | ❌ 不填 |
+| **B2C 个人 / 用载具** | ❌ 不填 | ⚪ 可不填(走载具就不发邮箱) | ✅ 必填(类型 + 号码都要) |
+| **B2C 个人 / 系统查看** | ❌ 不填 | ❌ 不填 | ❌ 不填(PrintFlag=Y,靠发票号在系统查) |
+
+> 记忆口诀:
+> - `B2B` → 只要 **统编 buyerUbn**,其余(邮箱/载具)不用管。
+> - `B2C` → 邮箱 / 载具 / 都不填 **三选一**:填邮箱→发邮件;填载具→存载具;都不填→客户在系统查看发票(PrintFlag=Y)。
+> - 只要 `carrierType` 有值,`carrierNum` 就必须跟着填,否则报"载具号码不能为空"。
+> - ⚠️ 「系统查看」模式依赖 ezPay 接受空 BuyerEmail,上线前需在测试环境验证;若 ezPay 拒绝则回落为必填邮箱。
+
+**③ 四种场景请求示例**
+
+B2B 公司发票:
+```json
+{ "orderId": 1001, "category": "B2B", "buyerName": "美食達有限公司", "buyerUbn": "12345678" }
+```
+
+B2C 个人 / 邮箱接收:
+```json
+{ "orderId": 1001, "category": "B2C", "buyerName": "王小明", "buyerEmail": "xm@example.com" }
+```
+
+B2C 个人 / 手机条码载具:
+```json
+{ "orderId": 1001, "category": "B2C", "buyerName": "王小明", "carrierType": "0", "carrierNum": "/ABC1234" }
+```
+
+B2C 个人 / 系统查看(不填邮箱、不用载具):
+```json
+{ "orderId": 1001, "category": "B2C", "buyerName": "王小明" }
+```
 
 **处理**:校验订单归属与可开票(门店 ezPay 已开通启用、非免用发票)→ 金额拆分 → 组装明细 → 调 `EzPay.issueInvoice` → 落库发票号/随机码/凭证。
 

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů