Просмотр исходного кода

Merge remote-tracking branch 'origin/main' into main

luofeiyun 5 дней назад
Родитель
Сommit
5331e9f9d7
29 измененных файлов с 1261 добавлено и 162 удалено
  1. 1 0
      ship-module-resource/ship-module-resource-api/src/main/java/com/yc/ship/module/resource/enums/DictTypeConstants.java
  2. 5 0
      ship-module-trade/ship-module-trade-api/src/main/java/com/yc/ship/module/trade/enums/ApiConstants.java
  3. 1 0
      ship-module-trade/ship-module-trade-api/src/main/java/com/yc/ship/module/trade/enums/ErrorCodeConstants.java
  4. 3 3
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/insurance/InsuranceController.java
  5. 18 1
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/OtcTradeOrderController.java
  6. 1 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/OrderTotalRespVO.java
  7. 9 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/ShipTradeOrderCreateReqVO.java
  8. 73 2
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TouristExportVisitorVO.java
  9. 9 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderPdaRespVO.java
  10. 9 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderRespNewVO.java
  11. 9 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderRespVO.java
  12. 9 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/order/TradeOrderDO.java
  13. 11 4
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/framework/mq/TradeMqReceiver.java
  14. 3 11
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/framework/mq/TradePublishUtils.java
  15. 11 6
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/job/InsuranceJob.java
  16. 9 6
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/insurance/InsuranceService.java
  17. 118 31
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/insurance/InsuranceServiceImpl.java
  18. 203 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/invoice/impl/InvoiceGroupServiceImpl.java
  19. 40 1
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/order/impl/AdminTradeOrderServiceImpl.java
  20. 7 1
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/OtcTradeOrderService.java
  21. 434 75
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/impl/OtcTradeOrderServiceImpl.java
  22. 7 1
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/pay/impl/TradeOrderPayServiceImpl.java
  23. 0 4
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/refund/impl/TradeRefundServiceImpl.java
  24. 36 0
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/InsuranceUtil.java
  25. 196 3
      ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/excel/ExcelStyleHandler.java
  26. 4 2
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeOrderMapper.xml
  27. 35 11
      ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeVisitorMapper.xml
  28. BIN
      ship-module-trade/ship-module-trade-biz/src/main/resources/templates/tourist_template.xlsx
  29. BIN
      ship-module-trade/ship-module-trade-biz/src/main/resources/templates/tourist_template_operator.xlsx

+ 1 - 0
ship-module-resource/ship-module-resource-api/src/main/java/com/yc/ship/module/resource/enums/DictTypeConstants.java

@@ -36,6 +36,7 @@ public interface DictTypeConstants {
 
     String VISITOR_CREDENTIAL_TYPE = "credential_type"; //运营一体化 - 游客证件类型
     String VOUCHER_STATUS = "voucher_status"; //运营一体化 - 票券状态
+    String TOUR_TYPE = "tour_type"; //运营一体化 - 游客类型
 
     String TAXI_INVOICE_STATUS = "taxi_invoice_status"; //出租车票申请单状态
 

+ 5 - 0
ship-module-trade/ship-module-trade-api/src/main/java/com/yc/ship/module/trade/enums/ApiConstants.java

@@ -122,6 +122,11 @@ public class ApiConstants {
      */
     public static final int PAY_ORDER_TYPE_OTHER = 1;
 
+    /**
+     * 支付单-订单类型 定金支付
+     */
+    public static final int PAY_ORDER_TYPE_DEPOSI = 2;
+
     /**
      * redis 缓存,时间方案最小时间
      */

+ 1 - 0
ship-module-trade/ship-module-trade-api/src/main/java/com/yc/ship/module/trade/enums/ErrorCodeConstants.java

@@ -34,6 +34,7 @@ public interface ErrorCodeConstants {
     ErrorCode OPERATE_PASSWORD_FAILED = new ErrorCode(15_001_1, "操作密码校验失败");
     ErrorCode PASSWORD_DECRYPT_FAILED = new ErrorCode(15_001_2, "操作密码解密失败");
     ErrorCode INSURANCE_NOT_EXISTS = new ErrorCode(15_002, "订单保险信息不存在");
+    ErrorCode ORDER_NOT_EXISTS = new ErrorCode(15_002, "保险订单不存在");
     ErrorCode INSURE_FAILED = new ErrorCode(15_002_01, "保险投保失败");
     ErrorCode INSURE_CANCEL_FAILED = new ErrorCode(15_002_02, "保险退保失败");
 

+ 3 - 3
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/insurance/InsuranceController.java

@@ -83,7 +83,7 @@ public class InsuranceController {
     @OperateLog(type = API)
     @PreAuthorize("@ss.hasPermission('trade:insurance:query')")
     public CommonResult queryEpolicyList(@RequestParam("id") Long id) {
-        insuranceService.queryEpolicyList(id);
+        insuranceService.queryInsuranceByOrderId(id);
         return success(true);
     }
 
@@ -92,8 +92,8 @@ public class InsuranceController {
     @Operation(summary = "退保")
     @OperateLog(type = API)
     @PreAuthorize("@ss.hasPermission('trade:insurance:apply')")
-    public CommonResult<Boolean>  cancelInsurance(@RequestParam("orderId") Long orderId){
-        insuranceService.applyCancelInsurance(orderId);
+    public CommonResult<Boolean>  cancelInsurance(@RequestParam("id") Long id){
+        insuranceService.applyCancelInsurance(id);
         return success(true);
     }
 }

+ 18 - 1
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/OtcTradeOrderController.java

@@ -627,12 +627,13 @@ public class OtcTradeOrderController {
     }
 
     /**
-     * 导出游客名单
+     * 导出游客名单-计调
      */
     @GetMapping("/export-touristExcel")
     @Operation(summary = "导出游客名单 Excel")
     @OperateLog(type = EXPORT, enable = false)
     @PlatTenantEnv
+    @PreAuthorize("@ss.hasPermission('trade:order:export-operator')")
     public void exportTouristList(@Valid TradeOrderPageReqVO pageReqVO, HttpServletResponse response) throws IOException {
         File tempFile = otcTradeOrderService.exportTouristList(pageReqVO);
         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -640,4 +641,20 @@ public class OtcTradeOrderController {
         InputStream is = Files.newInputStream(tempFile.toPath());
         IOUtils.copy(is, response.getOutputStream());
     }
+
+    /**
+     * 导出游客名单-代理商
+     */
+    @GetMapping("/export-touristExcel-agent")
+    @Operation(summary = "导出游客名单 Excel")
+    @OperateLog(type = EXPORT, enable = false)
+    @PlatTenantEnv
+    @PreAuthorize("@ss.hasPermission('trade:order:export-agent')")
+    public void exportTouristListToAgent(@Valid TradeOrderPageReqVO pageReqVO, HttpServletResponse response) throws IOException {
+        File tempFile = otcTradeOrderService.exportTouristListToAgent(pageReqVO);
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-Disposition", "attachment; filename=" + tempFile.getName());
+        InputStream is = Files.newInputStream(tempFile.toPath());
+        IOUtils.copy(is, response.getOutputStream());
+    }
 }

+ 1 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/OrderTotalRespVO.java

@@ -104,6 +104,7 @@ public class OrderTotalRespVO {
     @Schema(description = "定金")
     private BigDecimal deposi;
 
+
     @Schema(description = "受损金额")
     private BigDecimal damaged;
 

+ 9 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/ShipTradeOrderCreateReqVO.java

@@ -90,6 +90,15 @@ public class ShipTradeOrderCreateReqVO implements Serializable {
     @Schema(description = "定金")
     private BigDecimal deposi;
 
+    @Schema(description = "实付已经金额")
+    private BigDecimal realPayAmount;
+
+    @Schema(description = "政策优惠金额")
+    private BigDecimal freeAmount;
+
+    @Schema(description = "是否需要补缴费 1是 0 否")
+    private Integer isSupplementary;
+
     @Schema(description = "受损金额")
     private BigDecimal damaged;
 

+ 73 - 2
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TouristExportVisitorVO.java

@@ -3,6 +3,8 @@ package com.yc.ship.module.trade.controller.admin.order.vo.order;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 游客名单导出 - 游客信息VO
  */
@@ -10,11 +12,62 @@ import lombok.Data;
 @Data
 public class TouristExportVisitorVO {
 
+    // ==================== 订单详情 ====================
+    @Schema(description = "代理商")
+    private String sourceName;
+
+    @Schema(description = "订单号")
+    private String orderNo;
+
+    @Schema(description = "团号")
+    private String groupNo;
+
+    @Schema(description = "航向")
+    private String direction;
+
+    @Schema(description = "航期")
+    private String travelDate;
+
+    @Schema(description = "应收款")
+    private BigDecimal amount;
+
+    @Schema(description = "实收款")
+    private BigDecimal payAmount;
+
+    @Schema(description = "定金")
+    private BigDecimal deposi;
+
+    @Schema(description = "支付状态(1=已支付,其他=未支付)")
+    private Integer payStatus;
+
+    @Schema(description = "接站")
+    private String jz;
+
+    @Schema(description = "订单状态")
+    private String orderStatus;
+
+    @Schema(description = "订单id")
+    private String orderId;
+
+    // ==================== 房间详情 ====================
+    @Schema(description = "房间索引ID(用于合并)")
+    private String roomIndexId;
+
+    @Schema(description = "序号(按订单分组)")
+    private Integer roomIndex;
+
+    @Schema(description = "房型")
+    private String roomType;
+
+    @Schema(description = "入住类型(房型描述)")
+    private String roomDescription;
+
+    // ==================== 游客详情 ====================
     @Schema(description = "游客姓名")
     private String name;
 
-    @Schema(description = "国籍名称")
-    private String nationalityName;
+    @Schema(description = "性别")
+    private Integer gender;
 
     @Schema(description = "证件类型")
     private Integer credentialType;
@@ -22,9 +75,27 @@ public class TouristExportVisitorVO {
     @Schema(description = "证件号")
     private String credentialNo;
 
+    @Schema(description = "游客类型")
+    private String visitorType;
+
+    @Schema(description = "国籍名称")
+    private String nationalityName;
+
     @Schema(description = "生日")
     private String birthday;
 
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "楼层")
+    private String floor;
+
+    @Schema(description = "增值服务")
+    private String valueAddedService;
+
+    @Schema(description = "优惠政策")
+    private String policyName;
+
     @Schema(description = "备注")
     private String remark;
 }

+ 9 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderPdaRespVO.java

@@ -441,6 +441,15 @@ public class TradeOrderPdaRespVO {
     @Schema(description = "定金")
     private BigDecimal deposi;
 
+    @Schema(description = "实付已经金额")
+    private BigDecimal realPayAmount;
+
+    @Schema(description = "政策优惠金额")
+    private BigDecimal freeAmount;
+
+    @Schema(description = "是否需要补缴费 1是 0 否")
+    private Integer isSupplementary;
+
     @Schema(description = "受损金额")
     private BigDecimal damaged;
 

+ 9 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderRespNewVO.java

@@ -215,6 +215,15 @@ public class TradeOrderRespNewVO {
     @Schema(description = "定金")
     private BigDecimal deposi;
 
+    @Schema(description = "实付已经金额")
+    private BigDecimal realPayAmount;
+
+    @Schema(description = "政策优惠金额")
+    private BigDecimal freeAmount;
+
+    @Schema(description = "是否需要补缴费 1是 0 否")
+    private Integer isSupplementary;
+
     @Schema(description = "受损金额")
     private BigDecimal damaged;
 

+ 9 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/controller/admin/order/vo/order/TradeOrderRespVO.java

@@ -526,6 +526,15 @@ public class TradeOrderRespVO {
     @Schema(description = "定金")
     private BigDecimal deposi;
 
+    @Schema(description = "实付已经金额")
+    private BigDecimal realPayAmount;
+
+    @Schema(description = "政策优惠金额")
+    private BigDecimal freeAmount;
+
+    @Schema(description = "是否需要补缴费 1是 0 否")
+    private Integer isSupplementary;
+
     @Schema(description = "受损金额")
     private BigDecimal damaged;
 

+ 9 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/dataobject/order/TradeOrderDO.java

@@ -336,6 +336,15 @@ public class TradeOrderDO extends TenantBaseDO {
     @ForUpdate(fieldName = "定金")
     private BigDecimal deposi;
 
+    @ForUpdate(fieldName = "实付已经金额")
+    private BigDecimal realPayAmount;
+
+    @ForUpdate(fieldName = "政策优惠金额")
+    private BigDecimal freeAmount;
+
+    @ForUpdate(fieldName = "是否需要补缴费 1是 0 否")
+    private Integer isSupplementary;
+
     @ForUpdate(fieldName = "受损金额")
     private BigDecimal damaged;
 

+ 11 - 4
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/framework/mq/TradeMqReceiver.java

@@ -371,42 +371,49 @@ public class TradeMqReceiver {
 
     @RabbitListener(queues = TradeMqConfig.DL_QUEUE_INSURANCE_QUERY, concurrency = "2")
     @TenantIgnore
-    public void processInsuranceQuery(String data) {
+    public void processInsuranceQuery(String data, Message message, Channel channel) throws IOException {
         log.error("收到保险查询结果消息 :" + data);
         try {
             CommonResult commonResult = insuranceUtil.queryInsurance(data);
             if (!commonResult.isSuccess()) {
                 log.error("保险查询结果消息通知出现错误{}", commonResult.getMsg());
-                tradePublishUtils.publishInsuranceQueryMsg(data, 1000*60);
+                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
             } else {
                 String checkedData = (String) commonResult.getCheckedData();
+                log.error("保险查询结果消息通知返回数据{}", checkedData);
                 JSONObject jsonObject = JSONObject.parseObject(checkedData);
                 BigDecimal paiedAmount = jsonObject.getBigDecimal("paiedAmount");
                 JSONArray policies = jsonObject.getJSONArray("policies");
                 if(policies == null) {
+                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
                     return;
                 }
                 JSONObject policy = policies.getJSONObject(0);
                 if(policy == null) {
+                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
                     return;
                 }
                 String status = policy.getString("status");
                 if("PROCESSING".equals(status)) {
                     log.error("保险查询结果,投保中,继续查询");
-                    tradePublishUtils.publishInsuranceQueryMsg(data, 1000 * 60);
+                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
                 } else if("FAIL".equals(status)) {
                     log.error("保险查询结果,投保失败,不再查询");
+                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
                 } else if("SUCCESS".equals(status)) {
                     log.error("保险查询结果,投保成功,不再查询");
                     Long externalPolicyNumber = policy.getLong("externalPolicyNumber");
                     String policyNo = policy.getString("policyNo");
                     insuranceService.handleInsuranceQuery(paiedAmount, policyNo, externalPolicyNumber, status);
+                    channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
                 }
             }
-
+            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
         } catch (Exception e) {
             log.error("保险查询结果消息MQ通知出现错误", e);
+            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
         }
 
+
     }
 }

+ 3 - 11
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/framework/mq/TradePublishUtils.java

@@ -218,16 +218,8 @@ public class TradePublishUtils {
         }
     }
 
-    /**
-     * 发送保险查询通知
-     * @param data
-     * @param delay
-     */
-    public void publishInsuranceQueryMsg(String data, long delay){
-        log.info("发送结算消息OrderId:{}, 延时毫秒:{},发送时间:{}", data, delay, DateUtil.now());
-        template.convertAndSend(DL_EXCHANGE_INSURANCE, DL_QUEUE_INSURANCE_QUERY, data, message -> {
-            message.getMessageProperties().setHeader("x-delay",delay == 0 ? 60 * 1000 : delay);
-            return message;
-        });
+
+    public void publishInsuranceQueryMsg(String message){
+        template.convertAndSend(EXCHANGE_ORDER, DL_QUEUE_INSURANCE_QUERY, message);
     }
 }

+ 11 - 6
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/job/InsuranceJob.java

@@ -1,9 +1,12 @@
 package com.yc.ship.module.trade.job;
 
 import com.yc.ship.framework.quartz.core.handler.JobHandler;
+import com.yc.ship.module.trade.service.insurance.InsuranceService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.Resource;
+
 /**
  * 保险购买定时任务
  * 1. 查询需要购买保险的订单,只有确认状态并且开航前一天才可购买保险
@@ -11,17 +14,19 @@ import org.springframework.stereotype.Component;
 @Component
 @Slf4j
 public class InsuranceJob implements JobHandler {
+
+    @Resource
+    private InsuranceService insuranceService;
+
     /**
      * 执行定时任务
-     * 1.该定时任务会每天去查询开航前一天的航次,
-     * 2.根据航次去查询确认状态的订单,
-     * 3.购买保险
-     * @param param 传入的参数(当前未使用)
-     * @return 固定返回字符串"成功"
-     * @throws Exception 任务异常
+     * 定时查询保险状态
      */
     @Override
     public String execute(String param) {
+        log.info("开始执行保单查询定时任务");
+        insuranceService.queryInsuranceQuey();
+        log.info("结束执行保单查询定时任务");
         return "success";
     }
 }

+ 9 - 6
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/insurance/InsuranceService.java

@@ -1,10 +1,7 @@
 package com.yc.ship.module.trade.service.insurance;
 
 
-import com.yc.ship.framework.common.pojo.CommonResult;
 import com.yc.ship.framework.common.pojo.PageResult;
-import com.yc.ship.module.trade.api.insurance.dto.InsuranceApplyReqDTO;
-import com.yc.ship.module.trade.api.insurance.dto.InsuredRespDTO;
 import com.yc.ship.module.trade.controller.admin.insurance.vo.InsurancePageReqVO;
 import com.yc.ship.module.trade.controller.admin.insurance.vo.InsuranceRespVO;
 import com.yc.ship.module.trade.dal.dataobject.insurance.InsuranceDO;
@@ -19,8 +16,8 @@ import java.util.List;
  */
 public interface InsuranceService {
 
-    String INSURANCE_KEY ="order_insurance:apply_";
-    String INSURANCE_CANCEL_KEY ="order_insurance:cancel_";
+    String INSURANCE_KEY = "order_insurance:apply_";
+    String INSURANCE_CANCEL_KEY = "order_insurance:cancel_";
 
     /**
      * 获得订单保险信息
@@ -32,6 +29,7 @@ public interface InsuranceService {
 
     /**
      * 通过订单id获取保险信息
+     *
      * @param orderIdList
      * @return
      */
@@ -55,12 +53,17 @@ public interface InsuranceService {
     /**
      * 取消投保
      */
-    void applyCancelInsurance(Long orderId);
+    void applyCancelInsurance(Long id);
+
+    void queryInsuranceByOrderId(Long id);
+
+    void queryInsuranceQuey();
 
     /**
      * 查询电子保单
      */
     void queryEpolicyList(Long id);
+
     void queryEpolicyAll();
 
     InsuranceDO getByOrderId(Long orderId);

+ 118 - 31
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/insurance/InsuranceServiceImpl.java

@@ -6,6 +6,8 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@@ -17,7 +19,6 @@ import com.yc.ship.module.infra.api.config.ConfigApi;
 import com.yc.ship.module.product.dal.dataobject.voyage.VoyageDO;
 import com.yc.ship.module.product.service.voyage.VoyageService;
 import com.yc.ship.module.trade.api.insurance.dto.*;
-import com.yc.ship.module.trade.controller.admin.insurance.vo.InsuranceData;
 import com.yc.ship.module.trade.controller.admin.insurance.vo.InsurancePageReqVO;
 import com.yc.ship.module.trade.controller.admin.insurance.vo.InsuranceRespVO;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.TradeOrderRespVO;
@@ -32,7 +33,6 @@ import com.yc.ship.module.trade.framework.mq.TradePublishUtils;
 import com.yc.ship.module.trade.utils.InsuranceRequestHelper;
 import com.yc.ship.module.trade.utils.InsuranceUtil;
 import lombok.extern.slf4j.Slf4j;
-import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
@@ -41,15 +41,16 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
 import static com.yc.ship.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static com.yc.ship.framework.common.exception.util.ServiceExceptionUtil.exception0;
-import static com.yc.ship.module.trade.enums.ErrorCodeConstants.*;
+import static com.yc.ship.module.trade.enums.ErrorCodeConstants.INSURANCE_NOT_EXISTS;
+import static com.yc.ship.module.trade.enums.ErrorCodeConstants.ORDER_NOT_EXISTS;
 
 
 /**
@@ -181,35 +182,36 @@ public class InsuranceServiceImpl implements InsuranceService {
 
         //验证投保信息
         CommonResult commonResult = insuranceUtil.validateInsuranceRequest(insuranceApplyReqDTO);
-        if(!commonResult.isSuccess()) {
-            throw exception0(commonResult.getCode(),commonResult.getMsg());
+        if (!commonResult.isSuccess()) {
+            throw exception0(commonResult.getCode(), commonResult.getMsg());
         }
         // 开始投保
         commonResult = insuranceUtil.sendInsuranceApply(insuranceApplyReqDTO);
-        if(!commonResult.isSuccess()) {
-            throw exception0(commonResult.getCode(),commonResult.getMsg());
+        if (!commonResult.isSuccess()) {
+            throw exception0(commonResult.getCode(), commonResult.getMsg());
         }
         //保存投保信息
         InsuranceDO insuranceDO = new InsuranceDO();
         insuranceDO.setOrderId(orderId);
-//            insuranceDO.setAgencyOrderId(applyReqVO.getAgencyOrderId());
         insuranceDO.setInsuranceStatus(InsuranceStatusEnum.INSURE.getValue());
         insuranceDO.setInsuredNum(insuredList.size());
-//            insuranceDO.setHolderName(applyReqVO.getHolderName());
-//            insuranceDO.setHolderNo(applyReqVO.getHolderNo());
-//            insuranceDO.setRationType(applyReqVO.getRationType());
-//            insuranceDO.setRiskCode(riskCode);
-//            insuranceDO.setInsuranceEffectDate(LocalDateTime.ofInstant(applyReqVO.getTravelDate().toInstant(), ZoneId.systemDefault()));
+        insuranceDO.setInsuranceNo(orderInfo.getOrderNo());
+        insuranceDO.setInsuranceNo(orderInfo.getOrderNo());
+        insuranceDO.setTenantId(voyage.getTenantId());
+        insuranceDO.setRationType(insuranceOrderInfoDTO.getProductNo());
+        insuranceDO.setResMsg(String.valueOf(commonResult.getCheckedData()));
+        insuranceDO.setInsuranceEffectDate(LocalDateTime.parse(DateUtil.format(voyage.getBoardingTime(), "yyyy-MM-dd")));
         Long id = IdWorker.getId(insuranceDO);
         insuranceDO.setId(id);
         insuranceMapper.insert(insuranceDO);
         // 发送查询投保接口通知
-        tradePublishUtils.publishInsuranceQueryMsg(orderInfo.getOrderNo(),0);
+        tradePublishUtils.publishInsuranceQueryMsg(orderInfo.getOrderNo());
         return true;
     }
 
     /**
      * 处理系统证件枚举与保险证件枚举转换
+     *
      * @param credentialType
      * @return
      */
@@ -222,7 +224,14 @@ public class InsuranceServiceImpl implements InsuranceService {
             case 1:
                 transCredentialType = 3;
                 break;
-            case 2: case 3: case 4: case 6: case 7: case 8: case 9: case 99:
+            case 2:
+            case 3:
+            case 4:
+            case 6:
+            case 7:
+            case 8:
+            case 9:
+            case 99:
                 transCredentialType = 6;
                 break;
             case 5:
@@ -235,17 +244,20 @@ public class InsuranceServiceImpl implements InsuranceService {
     /**
      * 退保
      *
-     * @param orderId
      */
     @Override
     @Transactional
-    public void applyCancelInsurance(Long orderId) {
-        TradeOrderRespVO orderInfo = tradeOrderMapper.getOrderInfo(orderId);
+    public void applyCancelInsurance(Long id) {
+        InsuranceDO insuranceDO = insuranceMapper.selectById(id);
+        if(insuranceDO == null){
+            throw exception(INSURANCE_NOT_EXISTS);
+        }
+        TradeOrderRespVO orderInfo = tradeOrderMapper.getOrderInfo(insuranceDO.getOrderId());
+        if(orderInfo == null){
+            throw exception(ORDER_NOT_EXISTS);
+        }
         VoyageDO voyage = voyageService.getVoyage(orderInfo.getVoyageId());
 
-        //获取投保记录,投保保单号
-        InsuranceDO insuranceDO = insuranceMapper.selectByOrderId(orderId);
-
         InsuranceCancelReqDTO insuranceCancelReqDTO = new InsuranceCancelReqDTO();
         insuranceCancelReqDTO.setService("cancel");
         insuranceCancelReqDTO.setReplyUrl(notifyUrl);
@@ -267,8 +279,8 @@ public class InsuranceServiceImpl implements InsuranceService {
 
         // 开始退保
         CommonResult commonResult = insuranceUtil.sendInsuranceCancel(insuranceCancelReqDTO);
-        if(!commonResult.isSuccess()) {
-            throw exception0(commonResult.getCode(),commonResult.getMsg());
+        if (!commonResult.isSuccess()) {
+            throw exception0(commonResult.getCode(), commonResult.getMsg());
         }
     }
 
@@ -297,11 +309,11 @@ public class InsuranceServiceImpl implements InsuranceService {
         insuranceDO.setPremium(amount);
         insuranceDO.setPolicyNo(policyNo);
         Integer insuranceStatus = InsuranceStatusEnum.INSURE.getValue();
-        if("SUCCESS".equals(status)) {
+        if ("SUCCESS".equals(status)) {
             insuranceStatus = InsuranceStatusEnum.SUCCESS.getValue();
-        } else if ("FAIL".equals(status)){
+        } else if ("FAIL".equals(status)) {
             insuranceStatus = InsuranceStatusEnum.FAIL.getValue();
-        }else if ("PROCESSING".equals(status)){
+        } else if ("PROCESSING".equals(status)) {
             insuranceStatus = InsuranceStatusEnum.INSURE.getValue();
         }
         insuranceDO.setInsuranceStatus(insuranceStatus);
@@ -311,6 +323,81 @@ public class InsuranceServiceImpl implements InsuranceService {
         tradeOrderMapper.updateById(tradeOrderDO);
     }
 
+    @Override
+    public void queryInsuranceByOrderId(Long id) {
+        InsuranceDO insuranceDO = insuranceMapper.selectById(id);
+        CommonResult commonResult = insuranceUtil.queryInsuranceNew(insuranceDO.getInsuranceNo(),insuranceDO.getOrderId());
+        if (!commonResult.isSuccess()) {
+            throw exception0(commonResult.getCode(), commonResult.getMsg());
+        }
+        //{"phase":"INSURE","externalOrderNo":"tys-20261001-YC-14",
+        // "paiedAmount":"10.00","total":"1","insuredCount":"1","insureProcessingCount":"0",
+        // "cancelledCount":"0","cancelProcessingCount":"0","refundedAmount":"0.0",
+        // "cipher":"26e2e422d370653274ee74ef3edc7f93","policies":[{"externalPolicyNumber":"2037220248460828673",
+        // "service":"applyTeam","status":"SUCCESS","msg":"投保成功","policyNo":"HW61927008L7JB7J8Q00"}]}
+        String checkedData = (String) commonResult.getCheckedData();
+        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(checkedData);
+        BigDecimal paiedAmount = jsonObject.getBigDecimal("paiedAmount");
+        JSONArray policies = jsonObject.getJSONArray("policies");
+        if(policies == null) {
+            handleInsuranceQuery(paiedAmount, "", insuranceDO.getOrderId(), "FAIL");
+        }
+        com.alibaba.fastjson.JSONObject policy = policies.getJSONObject(0);
+        if(policy == null) {
+            handleInsuranceQuery(paiedAmount, "", insuranceDO.getOrderId(), "FAIL");
+        }
+        String status = policy.getString("status");
+        if("PROCESSING".equals(status)) {
+            log.error("保险查询结果,投保中,继续查询"+insuranceDO.getInsuranceNo());
+        } else if("FAIL".equals(status)) {
+            log.error("保险查询结果,投保失败,不再查询"+insuranceDO.getInsuranceNo());
+        } else if("SUCCESS".equals(status)) {
+            log.error("保险查询结果,投保成功,不再查询"+insuranceDO.getInsuranceNo());
+            Long externalPolicyNumber = policy.getLong("externalPolicyNumber");
+            String policyNo = policy.getString("policyNo");
+            handleInsuranceQuery(paiedAmount, policyNo, externalPolicyNumber, status);
+        }
+    }
+
+
+    @Override
+    public void queryInsuranceQuey() {
+        List<InsuranceDO> list = insuranceMapper.selectList(new LambdaQueryWrapper<InsuranceDO>().eq(InsuranceDO::getInsuranceStatus, 0).ge(InsuranceDO::getCreateTime, DateUtil.yesterday()));
+        for (InsuranceDO insuranceDO : list) {
+            CommonResult commonResult = insuranceUtil.queryInsuranceNew(insuranceDO.getInsuranceNo(), insuranceDO.getOrderId());
+            if (!commonResult.isSuccess()) {
+                throw exception0(commonResult.getCode(), commonResult.getMsg());
+            }
+            //{"phase":"INSURE","externalOrderNo":"tys-20261001-YC-14",
+            // "paiedAmount":"10.00","total":"1","insuredCount":"1","insureProcessingCount":"0",
+            // "cancelledCount":"0","cancelProcessingCount":"0","refundedAmount":"0.0",
+            // "cipher":"26e2e422d370653274ee74ef3edc7f93","policies":[{"externalPolicyNumber":"2037220248460828673",
+            // "service":"applyTeam","status":"SUCCESS","msg":"投保成功","policyNo":"HW61927008L7JB7J8Q00"}]}
+            String checkedData = (String) commonResult.getCheckedData();
+            com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(checkedData);
+            BigDecimal paiedAmount = jsonObject.getBigDecimal("paiedAmount");
+            JSONArray policies = jsonObject.getJSONArray("policies");
+            if(policies == null) {
+                handleInsuranceQuery(paiedAmount, "", insuranceDO.getOrderId(), "FAIL");
+            }
+            com.alibaba.fastjson.JSONObject policy = policies.getJSONObject(0);
+            if(policy == null) {
+                handleInsuranceQuery(paiedAmount, "", insuranceDO.getOrderId(), "FAIL");
+            }
+            String status = policy.getString("status");
+            if("PROCESSING".equals(status)) {
+                log.error("保险查询结果,投保中,继续查询"+insuranceDO.getInsuranceNo());
+            } else if("FAIL".equals(status)) {
+                log.error("保险查询结果,投保失败,不再查询"+insuranceDO.getInsuranceNo());
+            } else if("SUCCESS".equals(status)) {
+                log.error("保险查询结果,投保成功,不再查询"+insuranceDO.getInsuranceNo());
+                Long externalPolicyNumber = policy.getLong("externalPolicyNumber");
+                String policyNo = policy.getString("policyNo");
+                handleInsuranceQuery(paiedAmount, policyNo, externalPolicyNumber, status);
+            }
+        }
+    }
+
     /**
      * 电子保单查询
      */
@@ -351,14 +438,14 @@ public class InsuranceServiceImpl implements InsuranceService {
             //加密加签
             InsuranceRequestHelper.encryptAndSign(insuranceRequest, publicKey, privateKey);
             String url = configApi.getConfigValueByKey("insurance.url");
-            log.error("电子保险查询 data=" + JSONUtil.toJsonStr(insuranceRequest));
+            log.error("电子保险查询 data={}", JSONUtil.toJsonStr(insuranceRequest));
             String result = HttpUtil.createPost(url + "/QUERYEPOLICY").contentType("application/json").body(JSONUtil.toJsonStr(insuranceRequest)).timeout(15000).execute().body();
-            log.error("电子保险查询 result=" + result);
+            log.error("电子保险查询 result={}", result);
             JSONObject insuranceResponse = JSONUtil.parseObj(result);
 
             InsuranceRequestHelper.checkSignAndDecrypt(insuranceResponse, publicKey, privateKey);
             // 0:失败 1:成功
-            if (insuranceResponse != null && "200".equals(insuranceResponse.get("code"))) {
+            if ("200".equals(insuranceResponse.get("code"))) {
                 String data = insuranceResponse.get("data").toString();
                 if (data != null) {
                     Map queryEpolicyResult = JSONUtil.toBean(data, Map.class);
@@ -379,7 +466,7 @@ public class InsuranceServiceImpl implements InsuranceService {
                 }
             }
         } catch (Exception e) {
-            log.error("电子保单查询error:" + e.getMessage());
+            log.error("电子保单查询error:{}", e.getMessage());
         }
     }
 

+ 203 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/invoice/impl/InvoiceGroupServiceImpl.java

@@ -597,6 +597,209 @@ public class InvoiceGroupServiceImpl implements InvoiceGroupService {
         String salesName = configApi.getPlatformConfigValueByKey("newinvoice.sales.name");
         String batchNo = IdUtil.fastSimpleUUID();
 
+        JSONObject requestBody = new JSONObject();
+        JSONArray reqDDZ = new JSONArray();
+        JSONObject reqDDPC = new JSONObject();
+        //订单请求批次号
+        reqDDPC.set("DDQQPCH", batchNo);
+        //纳税人识别号
+        reqDDPC.set("NSRSBH", taxNum);
+        //发票种类代码01-专用发票 02-普通发票
+        if (invoiceList.get(0).getInvoiceType() == null) {
+            reqDDPC.set("FPLXDM", "002");
+        } else {
+            reqDDPC.set("FPLXDM", DigitalInvoiceTypeEnum.valueOf(invoiceList.get(0).getInvoiceType()).getCode());
+        }
+        requestBody.set("DDPCXX", reqDDPC);
+        invoiceList.forEach(invoice -> {
+            JSONObject jsonObject = new JSONObject();
+            JSONObject reqObj = new JSONObject();
+            //订单请求流水号
+            reqObj.set("DDQQLSH", invoice.getFpqqlsh());
+            //开票类型(0蓝字发票 1红字发票)
+            reqObj.set("KPLX", type);
+            //销货方纳税人识别号
+            reqObj.set("XHFSBH", taxNum);
+            //销货方名称
+            reqObj.set("XHFMC", salesName);
+            //销货方地址
+            reqObj.set("XHFDZ", "");
+            //销货方电话
+            reqObj.set("XHFDH", "");
+            //销货方银行名称
+            reqObj.set("XHFYH", configApi.getPlatformConfigValueByKey("newinvoice.sales.bank"));
+            //销货方银行账号
+            reqObj.set("XHFZH", configApi.getPlatformConfigValueByKey("newinvoice.sales.bankaccount"));
+
+            reqObj.set("DDH", invoice.getTransactionNo());
+
+            //开票员,以电子税局绑定人员为准
+            reqObj.set("KPR", invoice.getBuyerClerk());
+
+            //购买方编码
+            reqObj.set("GMFYH", "");
+            //购买方纳税人识别号
+            reqObj.set("GMFSBH", invoice.getBuyerTaxCode());
+            //购买方名称(当GMFBM为空并且开票方式为自动开票时必填。如果FPLXDM为002,且GMFLX为03(个人)时,GMFMC必须以(个人)字样结尾”(”为全角)
+            reqObj.set("GMFMC", invoice.getBuyerName());
+            //购买方地址
+            reqObj.set("GMFDZ", invoice.getBuyerAddress());
+            //购买方电话
+            reqObj.set("GMFDH", invoice.getBuyerPhone());
+            //购买方银行名称
+            reqObj.set("GMFYH", invoice.getBuyerBank());
+            //购买方电话
+            reqObj.set("GMFZH", invoice.getBuyerAccount());
+            //购买方类型(01企业,02机关事业单位,03个人,04其他,如不填写,默认为04)
+            if (invoice.getTitleType() != null) {
+                if (invoice.getTitleType() == 0) {
+                    //购买方名称(当GMFBM为空并且开票方式为自动开票时必填。如果FPLXDM为002,且GMFLX为03(个人)时,GMFMC必须以(个人)字样结尾”(”为全角)
+                    reqObj.set("GMFMC", invoice.getBuyerName() + "(个人)");
+                    reqObj.set("GMFLX", "03");
+                } else if (invoice.getTitleType() == 1) {
+                    reqObj.set("GMFLX", "01");
+                }
+            } else {
+                reqObj.set("GMFLX", "04");
+            }
+//            reqObj.set("GMFLX", invoice.getTitleType());
+            //购货方手机
+            reqObj.set("GMFSJH", invoice.getBuyerTel());
+            //购方邮箱,如果填写,发票类别为电子发票时会自动发送邮件
+            reqObj.set("GMFDZYX", invoice.getBuyerEmail());
+
+            JSONArray detailArray = new JSONArray();
+            List<InvoiceInventoryDO> inventoryList = invoiceInventoryService.getInvoiceInventoryListByInvoiceId(invoice.getId());
+            JSONObject detailObj;
+            BigDecimal taxExcludedAmount = BigDecimal.ZERO;
+            BigDecimal se = BigDecimal.ZERO;
+            BigDecimal taxIncludedAmount = BigDecimal.ZERO;
+            if (inventoryList.isEmpty()) {
+                int index = 1;
+                detailObj = new JSONObject();
+                detailObj.set("XH", index);
+                String GoodsCode = configApi.getPlatformConfigValueByKey("newinvoice.goods.code");
+                String spbm = StrUtil.padAfter(GoodsCode, 19, "0");
+                //商品编码
+                detailObj.set("SPBM", spbm);
+                String spmc = configApi.getPlatformConfigValueByKey("newinvoice.goods.simpleCode");
+                //项目名称
+                detailObj.set("XMMC", spmc);
+                //税率
+                BigDecimal taxRate = new BigDecimal(configApi.getPlatformConfigValueByKey("newinvoice.taxRate"));
+                detailObj.set("SL", taxRate);
+                //含税金额
+                taxIncludedAmount = invoice.getPrice();
+                if (type == 1) {
+                    taxIncludedAmount = BigDecimal.ZERO.subtract(invoice.getPrice());
+                }
+//                String dylzfpmxxh = String.valueOf(IdWorker.getId()).substring(0,8);
+//                detailObj.set("DYLZFPMXXH", dylzfpmxxh);
+                //不含税金额
+                taxExcludedAmount = taxIncludedAmount.divide(new BigDecimal(1).add(taxRate), 2, RoundingMode.HALF_UP);
+                //金额
+                detailObj.set("JE", taxIncludedAmount.setScale(2, RoundingMode.HALF_UP));
+                //税额
+                se = taxIncludedAmount.subtract(taxExcludedAmount);
+                detailObj.set("SE", se.setScale(2, RoundingMode.HALF_UP));
+                //扣除额
+                detailObj.set("KCE", "");
+                //行性质。0-正常行 1-折扣行 2-被折扣行
+                detailObj.set("FPHXZ", "0");
+                //含税标志
+                detailObj.set("HSBZ", "1");
+                //空:非零税率,1:免税,2:不征税 3:普通零税率
+                detailObj.set("LSLBS", "");
+                //0:不使用,1:使用,SPID为空时必填
+                detailObj.set("YHZCBS", 0);
+                detailArray.add(detailObj);
+
+                InvoiceInventorySaveReqVO saveReqVO = new InvoiceInventorySaveReqVO();
+                saveReqVO.setInvoiceId(invoice.getId());
+                saveReqVO.setSort(index);
+                saveReqVO.setAmount(taxIncludedAmount);
+                saveReqVO.setTax(se);
+                saveReqVO.setCode(spbm);
+                saveReqVO.setName(spmc);
+                saveReqVO.setTaxRate(taxRate);
+                saveReqVO.setFphxz(0);
+                saveReqVO.setHsbz(1);
+                saveReqVO.setYhzcbs(0);
+//                saveReqVO.setDylzfpmxxh(dylzfpmxxh);
+                InvoiceInventoryDO invoiceInventoryDO = invoiceInventoryService.createInvoiceInventory(saveReqVO);
+                inventoryList = new ArrayList<>(3);
+                inventoryList.add(invoiceInventoryDO);
+            } else {
+                for (InvoiceInventoryDO invoiceInventoryDO : inventoryList) {
+                    detailObj = new JSONObject();
+                    detailObj.set("XH", invoiceInventoryDO.getSort());
+                    //商品编码
+                    detailObj.set("SPBM", invoiceInventoryDO.getCode());
+                    //项目名称
+                    detailObj.set("XMMC", invoiceInventoryDO.getName());
+                    //税率
+                    detailObj.set("SL", invoiceInventoryDO.getTaxRate());
+                    //含税金额
+                    BigDecimal thisTaxIncludedAmount = invoiceInventoryDO.getAmount();
+                    if (type == 1) {
+                        detailObj.set("DYLZFPMXXH", invoiceInventoryDO.getDylzfpmxxh());
+                        thisTaxIncludedAmount = BigDecimal.ZERO.subtract(invoiceInventoryDO.getAmount());
+                    }
+                    //金额
+                    detailObj.set("JE", thisTaxIncludedAmount.setScale(2, RoundingMode.HALF_UP));
+                    //不含税金额
+                    BigDecimal thisTaxExcludedAmount = thisTaxIncludedAmount.divide(new BigDecimal(1).add(invoiceInventoryDO.getTaxRate()), 2, RoundingMode.HALF_UP);
+                    //税额
+                    BigDecimal thisSe = thisTaxIncludedAmount.subtract(thisTaxExcludedAmount);
+                    detailObj.set("SE", thisSe.setScale(2, RoundingMode.HALF_UP));
+                    //扣除额
+                    detailObj.set("KCE", invoiceInventoryDO.getDeductionAmount() == null ? "" : invoiceInventoryDO.getDeductionAmount());
+                    //行性质。0-正常行 1-折扣行 2-被折扣行
+                    detailObj.set("FPHXZ", invoiceInventoryDO.getFphxz());
+                    //含税标志
+                    detailObj.set("HSBZ", invoiceInventoryDO.getHsbz());
+                    //空:非零税率,1:免税,2:不征税 3:普通零税率
+                    detailObj.set("LSLBS", invoiceInventoryDO.getLslbs() == null ? "" : invoiceInventoryDO.getLslbs());
+                    //0:不使用,1:使用,SPID为空时必填
+                    detailObj.set("YHZCBS", invoiceInventoryDO.getYhzcbs());
+                    detailArray.add(detailObj);
+
+                    taxExcludedAmount = taxExcludedAmount.add(thisTaxExcludedAmount);
+                    se = se.add(thisSe);
+                    taxIncludedAmount = taxIncludedAmount.add(thisTaxIncludedAmount);
+                }
+            }
+            //合计金额
+            reqObj.set("HJJE", taxExcludedAmount.setScale(2, RoundingMode.HALF_UP));
+            //合计税额
+            reqObj.set("HJSE", se.setScale(2, RoundingMode.HALF_UP));
+            if (StringUtils.isNotBlank(remark)) {
+                //备注
+                reqObj.set("BZ", remark);
+            }
+            //价税合计
+            reqObj.set("JSHJ", taxIncludedAmount.setScale(2, RoundingMode.HALF_UP));
+            //系统来源
+            reqObj.set("SX_XTLY", StrUtil.isNotBlank(sysSource) ? sysSource : "SX_LYEQ");
+//            reqObj.set("DDMXXX", detailArray);
+            jsonObject.set("DDTXX", reqObj);
+            jsonObject.set("DDMXXX", detailArray);
+//            reqDDZ.add(reqObj);
+            reqDDZ.add(jsonObject);
+            invoice.setCHjse(se);
+            invoice.setCBhsje(taxExcludedAmount);
+        });
+        requestBody.set("DDZXX", reqDDZ);
+        return requestBody;
+    }
+
+
+
+    private JSONObject buildRequestParamNew(List<InvoiceDO> invoiceList, Integer type, String sysSource, String remark) {
+        String taxNum = configApi.getPlatformConfigValueByKey("newinvoice.group.tax");
+        String salesName = configApi.getPlatformConfigValueByKey("newinvoice.sales.name");
+        String batchNo = IdUtil.fastSimpleUUID();
+
         JSONObject requestBody = new JSONObject();
         JSONArray reqDDZ = new JSONArray();
         JSONObject reqDDPC = new JSONObject();

+ 40 - 1
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/order/impl/AdminTradeOrderServiceImpl.java

@@ -15,10 +15,15 @@ import com.yc.ship.framework.common.util.collection.CollectionUtils;
 import com.yc.ship.framework.common.util.object.ObjectUtils;
 import com.yc.ship.framework.mybatis.core.query.LambdaQueryWrapperX;
 import com.yc.ship.framework.security.core.LoginUser;
+import com.yc.ship.framework.security.core.util.SecurityFrameworkUtils;
 import com.yc.ship.module.ota.api.OtaDistributorApi;
 import com.yc.ship.module.ota.api.dto.DistributorRespDTO;
 import com.yc.ship.module.otc.api.agency.dto.AgencyUserLoginInfoRespDTO;
 import com.yc.ship.module.product.api.dto.ProductSpuRespDTO;
+import com.yc.ship.module.system.api.notify.NotifyMessageSendApi;
+import com.yc.ship.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
+import com.yc.ship.module.system.api.user.AdminUserApi;
+import com.yc.ship.module.system.api.user.dto.AdminUserRespDTO;
 import com.yc.ship.module.trade.api.contract.ContractApi;
 import com.yc.ship.module.trade.api.contract.dto.ContractApplyReqDTO;
 import com.yc.ship.module.trade.api.contract.dto.ContractTouristDTO;
@@ -101,6 +106,12 @@ public class AdminTradeOrderServiceImpl implements AdminTradeOrderService {
     @Resource
     private InvoiceService invoiceService;
 
+    @Resource
+    private NotifyMessageSendApi notifyMessageSendApi;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
     @Override
     public CommonResult<TradeRefundCreateRespVO> refund(LoginUser loginUser, TradeRefundCreateReqVO refundCreateReqVO) {
         AgencyUserLoginInfoRespDTO agencyUserLoginInfo = loginUser.getContext(AGENCY_LOGIN_INFO, AgencyUserLoginInfoRespDTO.class);
@@ -327,7 +338,7 @@ public class AdminTradeOrderServiceImpl implements AdminTradeOrderService {
         tradeOrderBindMapper.insert(tradeOrderBindDO);
 
         List<TradeDetailDO> tradeDetailList = tradeOrderRepositoryService.queryYcDetailByOrderId(orderId);
-        long between = DateUtil.between(new Date(),tradeOrderDO.getTravelDate(),  DateUnit.DAY,false);
+        long between = DateUtil.between(new Date(), tradeOrderDO.getTravelDate(), DateUnit.DAY, false);
         BigDecimal damaged;
         if (between > 21) {
             damaged = new BigDecimal(500).multiply(new BigDecimal(tradeDetailList.size()));
@@ -339,6 +350,34 @@ public class AdminTradeOrderServiceImpl implements AdminTradeOrderService {
 
         AuditUserDO auditUserDO = auditUserMapper.selectOne(new LambdaQueryWrapperX<AuditUserDO>().eq(AuditUserDO::getType, 4).eq(AuditUserDO::getAuditStatus, 1).eq(AuditUserDO::getDeleted, 0).orderByDesc(AuditUserDO::getCreateTime).last("limit 1"));
         tradeOrderMapper.update(new UpdateWrapper<TradeOrderDO>().set("audit_type", 4).set("audit_user", auditUserDO == null ? "" : auditUserDO.getAuditUser()).set("order_status", TradeOrderStatusEnum.CANCEL_AUDIT.getStatus()).set("audit_status", 1).set("damaged", damaged).set("damaged_status", 1).set("damaged_time", LocalDateTime.now()).eq("id", tradeOrderDO.getId()));
+
+        try {
+            Map map = new HashMap();
+            Long userId = SecurityFrameworkUtils.getLoginUserId();
+            map.put("orderNo", tradeOrderDO.getOrderNo());
+            map.put("oldOrderStatus", TradeOrderStatusEnum.valueOf(tradeOrderDO.getOrderStatus()).getName());
+            map.put("newOrderStatus", TradeOrderStatusEnum.CANCEL_AUDIT.getName());
+            AdminUserRespDTO user = adminUserApi.getUser(userId);
+            map.put("modifyUser", user.getNickname());
+            NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
+            reqDTO.setTemplateParams(map);
+            //订单({orderNo})已修改, 状态由{oldOrderStatus} 改为{newOrderStatus},  修改人为: {modifyUser} ,请注意核对!
+            //订单详情页面展示具体变更内容
+            reqDTO.setTemplateCode("sendmsg_cd_12");
+
+            List<Long> userIds = adminUserApi.getUserListByRoleCode("jdtx");
+            if (userIds != null) {
+                userIds.forEach(senduser -> {
+                    reqDTO.setUserId(senduser);
+                    notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
+                });
+            } else {
+                log.error("获取角色jdtx用户为空");
+            }
+        } catch (Exception e) {
+            log.error("发送站内信异常", e);
+        }
+
         TradeOrderLogUtils.setOrderInfo(orderId, tradeOrderDO.getOrderStatus(), TradeOrderStatusEnum.CANCEL_AUDIT.getStatus());
         return CommonResult.success("订单取消审核中");
     }

+ 7 - 1
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/OtcTradeOrderService.java

@@ -17,6 +17,7 @@ import com.yc.ship.module.trade.dal.dataobject.contract.ContractDO;
 import com.yc.ship.module.trade.dal.dataobject.insurance.InsuranceDO;
 import com.yc.ship.module.trade.dal.dataobject.order.TradeDetailBaseDO;
 
+import javax.validation.Valid;
 import java.io.File;
 import java.math.BigDecimal;
 import java.util.List;
@@ -152,7 +153,12 @@ public interface OtcTradeOrderService {
     OrderTotalRespVO getOrderTotal(OrderTotalQueryVO queryVO);
 
     /**
-     * 导出游客名单
+     * 导出游客名单-计调
      */
     File exportTouristList(TradeOrderPageReqVO pageReqVO);
+
+    /**
+     * 导出游客名单-代理商
+     */
+    File exportTouristListToAgent(@Valid TradeOrderPageReqVO pageReqVO);
 }

+ 434 - 75
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/otc/impl/OtcTradeOrderServiceImpl.java

@@ -110,6 +110,7 @@ import com.yc.ship.module.trade.utils.AgencyAuthUtils;
 import com.yc.ship.module.trade.utils.excel.ExcelStyleHandler;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.hpsf.Decimal;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.ss.util.RegionUtil;
@@ -422,7 +423,6 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                 AtomicReference<BigDecimal> amount = new AtomicReference<>(BigDecimal.ZERO);
                 try {
                     List<OrderPolicyDO> orderPolicyList = orderPolicyMapper.selectList(OrderPolicyDO::getOrderId, orderId);
-
                     if (!orderPolicyList.isEmpty()) {
                         orderPolicyList.forEach(item -> {
                             amount.set(amount.get().add(item.getAmount()));
@@ -439,6 +439,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                         .set(TradeOrderDO::getUpdateTime, LocalDateTime.now())
                         .set(TradeOrderDO::getDeposiStatus, 1)
                         .set(TradeOrderDO::getPayAmount, tradeOrderDO.getPayAmount().subtract(amount.get()))
+                        .set(TradeOrderDO::getFreeAmount, amount.get())
                         .eq(TradeOrderDO::getId, orderId)
                 );
                 Map<String, Object> extMap = new HashMap<>();
@@ -571,15 +572,8 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             tradeOrderAuditMapper.insert(tradeOrderAuditDO);
 
             if (tradeOrderDO.getAuditStatus() + 1 > tradeOrderDO.getAuditType()) {
-                tradeOrderMapper.update(Wrappers.<TradeOrderDO>lambdaUpdate()
-                        .set(TradeOrderDO::getAuditStatus, tradeOrderDO.getAuditStatus() + 1)
-                        .set(TradeOrderDO::getUpdateTime, LocalDateTime.now())
-                        .set(TradeOrderDO::getDamagedStatus, 2)
-                        .set(TradeOrderDO::getDamaged, damaged)
-                        .eq(TradeOrderDO::getId, orderId)
-                );
-                tradeOrderPayService.cancelOrder(orderId);
-
+                BigDecimal supplementAmount = tradeOrderDO.getRealPayAmount().subtract(damaged);
+                int isSupplement = supplementAmount.compareTo(BigDecimal.ZERO) > 0 ? 0 : 1;
                 try {
                     Map map = new HashMap();
                     map.put("orderNo", tradeOrderDO.getOrderNo());
@@ -588,23 +582,9 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                     ShipRespDTO shipRespDTO = shipApi.queryShip(tradeOrderDO.getShipId());
                     map.put("routeName", routeRespDTO.getName());
                     map.put("boatName", shipRespDTO.getName());
-                    BigDecimal deposi = tradeOrderDO.getDeposi();
-                    Integer deposiStatus = tradeOrderDO.getDeposiStatus();
-                    BigDecimal realPay = BigDecimal.ZERO;
-                    if (deposiStatus == 2) {
-                        realPay = deposi;
-                    } else if(tradeOrderDO.getPayStatus()==1){
-                        realPay = tradeOrderDO.getPayAmount();
-                    }
-
-                    BigDecimal payAmount = tradeOrderDO.getPayAmount();
-                    BigDecimal refundAmount = payAmount.multiply(damaged);
-
-                    BigDecimal multiply = refundAmount.multiply(realPay).compareTo(BigDecimal.ZERO) > 0 ? refundAmount.multiply(realPay):BigDecimal.ZERO;
-
                     map.put("wyAmount", damaged);
-                    map.put("wyRefundAmount", refundAmount);
-                    map.put("wyPayAmount", multiply);
+                    map.put("wyRefundAmount", isSupplement==0?supplementAmount:BigDecimal.ZERO);
+                    map.put("wyPayAmount", isSupplement==1?supplementAmount.abs():BigDecimal.ZERO);
                     map.put("startTime", DateUtil.formatDate(tradeOrderDO.getTravelDate()));
                     NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
                     reqDTO.setTemplateParams(map);
@@ -615,11 +595,28 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                     reqDTO.setUserId(Long.parseLong(tradeOrderDO.getSellerId()));
                     notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
                 } catch (Exception e) {
-                    log.error("发送信异常", e);
+                    log.error("发送站内信异常", e);
                 }
 
+                tradeOrderMapper.update(Wrappers.<TradeOrderDO>lambdaUpdate()
+                        .set(TradeOrderDO::getAuditStatus, tradeOrderDO.getAuditStatus() + 1)
+                        .set(TradeOrderDO::getUpdateTime, LocalDateTime.now())
+                        .set(TradeOrderDO::getDamagedStatus, 2)
+                        .set(TradeOrderDO::getSupplementAmount, supplementAmount)
+                        .set(TradeOrderDO::getDamaged, damaged)
+                        //是否需要补缴费 1是 0 否
+                        .set(TradeOrderDO::getIsSupplementary, isSupplement)
+                        .eq(TradeOrderDO::getId, orderId)
+                );
+                tradeOrderPayService.cancelOrder(orderId);
                 Map<String, Object> extMap = new HashMap<>();
-                extMap.put("result", "审核通过");
+                String result = "审核通过";
+                if(isSupplement==1){
+                    result += "需要补缴"+supplementAmount;
+                }else{
+                    result += "退押金"+supplementAmount;
+                }
+                extMap.put("result", result);
                 TradeOrderLogUtils.setOrderInfo(orderId, tradeOrderDO.getOrderStatus(), TradeOrderStatusEnum.UNPAID.getStatus(), extMap);
             } else {
                 AuditUserDO auditUserDO = auditUserMapper.selectOne(new QueryWrapper<AuditUserDO>().eq("type", tradeOrderDO.getAuditType()).eq("audit_status", tradeOrderDO.getAuditStatus() + 1).last("limit 1"));
@@ -1713,7 +1710,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
             for (TradeVistorReqVO tradeVistorReqVO : createVO.getTourist()) {
                 List<ShipTradeOrderCreateReqVO.Visitor> visitorList = new ArrayList<>();
                 ShipTradeOrderCreateReqVO.OrderDetail visitorDetailId = new ShipTradeOrderCreateReqVO.OrderDetail();
-                BeanUtils.copyProperties(orderDetail, ShipTradeOrderCreateReqVO.OrderDetail.class);
+                //BeanUtils.copyProperties(orderDetail, ShipTradeOrderCreateReqVO.OrderDetail.class);
                 visitorDetailId.setPrice(tradeVistorReqVO.getActualPrice());
                 visitorDetailId.setOriginPrice(tradeVistorReqVO.getPrice());
                 visitorDetailId.setProductType(0);
@@ -2684,47 +2681,198 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         return CommonResult.success("修改成功");
     }
 
+
+
+    private String getPersonTypeAll(String type) {
+        String des;
+        switch (type) {
+            case "leader":
+                des = "领队";
+                break;
+            case "with":
+                des = "陪同";
+                break;
+            case "childTake":
+            case "childPlus":
+            case "childNonTake":
+                des = "儿童";
+                break;
+            case "babyTake":
+            case "babyPlus":
+            case "babyNonTake":
+                des = "婴儿";
+                break;
+            case "adultTake":
+            case "adultPlus":
+            default:
+                des = "成人";
+                break;
+        }
+        return des;
+    }
+
+
+    /**
+     * 导出游客名单列表-计调
+     *
+     * 功能说明:
+     * 1. 查询订单基础信息(船名、航期、航向)
+     * 2. 查询游客列表(包含订单、房间、游客详细信息)
+     * 3. 使用Excel模板进行数据填充
+     * 4. 按订单和房间自动合并单元格
+     *
+     * 导出结构:
+     * - 订单详情(8列):代理商、订单号、团号、航向、航期、应收款、实收款、定金
+     * - 房间详情(3列):序号、房型、入住类型
+     * - 游客详情(8列):姓名、性别、证件类型、证件号、游客类型、国籍、增值服务、备注
+     *
+     * @param reqVO 查询条件
+     * @return 导出的Excel文件
+     */
     @Override
     public File exportTouristList(TradeOrderPageReqVO reqVO) {
+        InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template_operator.xlsx");
+        return getFile(reqVO,template, 0);
+    }
+
+    /**
+     * 导出游客名单列表-计调
+     *
+     * 功能说明:
+     * 1. 查询订单基础信息(船名、航期、航向)
+     * 2. 查询游客列表(包含订单、房间、游客详细信息)
+     * 3. 使用Excel模板进行数据填充
+     * 4. 按订单和房间自动合并单元格
+     *
+     * 导出结构:
+     * - 订单详情(8列):代理商、订单号、团号、航向、航期、应收款、实收款、定金
+     * - 房间详情(3列):序号、房型、入住类型
+     * - 游客详情(8列):姓名、性别、证件类型、证件号、游客类型、国籍、增值服务、备注
+     *
+     * @param reqVO 查询条件
+     * @return 导出的Excel文件
+     */
+    @Override
+    public File exportTouristListToAgent(TradeOrderPageReqVO reqVO) {
+        InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template_agent.xlsx");
+        return getFile(reqVO,template, 1);
+    }
+
+    private File getFile(TradeOrderPageReqVO reqVO,InputStream template, int fileType) {
+        List<Integer> orderStatus = reqVO.getOrderStatus();
+        // 排除状态为 -2 的订单
+        if (orderStatus != null && orderStatus.contains(-2)) {
+            orderStatus.remove(Integer.valueOf(-2));
+            reqVO.setOrderStatus(orderStatus.isEmpty() ? null : orderStatus);
+        }
+
         // 1. 查询订单基础信息(船名、航期、航向)
         Map<String, Object> baseInfo = tradeOrderMapper.selectTouristExportBase(reqVO);
-
-        // 2. 查询游客列表
+        // 查询订单表头信息
+        Map<String, Object> headInfo = tradeOrderMapper.selectTouristExportHead(reqVO);
+        // 2. 查询游客列表(包含订单、房间、游客详细信息)
         List<TouristExportVisitorVO> visitorList = tradeVisitorMapper.selectTouristExportVisitor(reqVO);
 
-        // 3. 加载模板
-        InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template.xlsx");
-        String fileName =  String.valueOf(System.currentTimeMillis());
+        // 3. 加载Excel模板
+        String fileName = String.valueOf(System.currentTimeMillis()) + fileType;
         String tmpFile = "/tmp/" + fileName + "_tourist.xlsx";
         //String tmpFile = "D:/tmp/" + fileName + "_tourist.xlsx";
 
-        // 先不合并,直接导出
+        // 4. 准备ExcelWriter,注册样式处理器和合并策略
+        //    - ExcelStyleHandler: 设置单元格样式
+        //    - TouristListMergeStrategy: 按订单和房间自动合并单元格
         ExcelWriter excelWriter = EasyExcel.write(tmpFile).withTemplate(template).build();
-        WriteSheet writeSheet = EasyExcelFactory.writerSheet().registerWriteHandler(new ExcelStyleHandler(9)).build();
+        WriteSheet writeSheet = EasyExcelFactory.writerSheet()
+                .registerWriteHandler(new ExcelStyleHandler(visitorList, fileType))
+                /* .registerWriteHandler(new TouristListMergeStrategy(visitorList))*/
+                .build();
         FillConfig fillConfig = FillConfig.builder().forceNewRow(true).autoStyle(true).build();
 
-        // 4. 准备基础信息数据
+        // 5. 准备基础信息数据(填充到Excel表头区域)
         Map<String, Object> baseData = new HashMap<>();
-
+        String personNum = "";
+        if(headInfo!=null){
+            // 总人数
+            String totalOrders = headInfo.get("totalOrders") != null ? headInfo.get("totalOrders").toString() :"0";
+            // 成人数
+            String adultNum = headInfo.get("adultNum") != null ? headInfo.get("adultNum").toString() :"0";
+            // 儿童数
+            String childBabyNum = headInfo.get("childBabyNum") != null ? headInfo.get("childBabyNum").toString() :"0";
+            // 陪同数
+            String withNum = headInfo.get("withNum") != null ? headInfo.get("withNum").toString() :"0";
+            // 领队数
+            String leaderNum = headInfo.get("leaderNum") != null ? headInfo.get("leaderNum").toString() :"0";
+            StringBuilder personNumBuilder = new StringBuilder();
+            if (StringUtils.isNotEmpty(totalOrders)) {
+                personNumBuilder.append(totalOrders);
+                if (StringUtils.isNotEmpty(adultNum) || StringUtils.isNotEmpty(childBabyNum)
+                        || StringUtils.isNotEmpty(withNum) || StringUtils.isNotEmpty(leaderNum)) {
+                    personNumBuilder.append("(");
+
+                    boolean hasContent = false;
+                    if (StringUtils.isNotEmpty(adultNum)) {
+                        personNumBuilder.append(adultNum).append(" 成人");
+                        hasContent = true;
+                    }
+                    if (StringUtils.isNotEmpty(childBabyNum)) {
+                        if (hasContent) {
+                            personNumBuilder.append("/");
+                        }
+                        personNumBuilder.append(childBabyNum).append(" 儿童");
+                        hasContent = true;
+                    }
+                    if (StringUtils.isNotEmpty(leaderNum)) {
+                        if (hasContent) {
+                            personNumBuilder.append("/");
+                        }
+                        personNumBuilder.append(leaderNum).append(" 领队");
+                        hasContent = true;
+                    }
+                    if (StringUtils.isNotEmpty(withNum)) {
+                        if (hasContent) {
+                            personNumBuilder.append("/");
+                        }
+                        personNumBuilder.append(withNum).append(" 陪同");
+                        hasContent = true;
+                    }
+                    personNumBuilder.append(")");
+                }
+            }
+            personNum = personNumBuilder.toString();
+        }
         // 船名
         baseData.put("shipName", baseInfo != null && baseInfo.get("shipName") != null ? baseInfo.get("shipName") : "");
-
-        // 航期
-        // 航期
-        baseData.put("travelDate", baseInfo != null && baseInfo.get("travelDate") != null
-                ? ((LocalDateTime) baseInfo.get("travelDate")).format(DateTimeFormatter.ofPattern("yyyy.M.d"))
-                : "");
-
-
-        // 航向
+        // 人数/类型
+        baseData.put("personNum", personNum);
+        // 人数/ 国籍
+        baseData.put("nationalityStats", headInfo != null && headInfo.get("nationalityStats") != null ? headInfo.get("nationalityStats") : "");
+        // 房间总数
+        baseData.put("totalRooms", headInfo != null && headInfo.get("totalRooms") != null ? headInfo.get("totalRooms") : "");
+        // 房型合计
+        baseData.put("roomStats", headInfo != null && headInfo.get("roomStats") != null ? headInfo.get("roomStats") : "");
+        // 应收款
+        baseData.put("totalPayAmount", headInfo != null && headInfo.get("totalPayAmount") != null ? headInfo.get("totalPayAmount") : "");
+        // 实收款
+        baseData.put("totalActualAmount", headInfo != null && headInfo.get("totalActualAmount") != null ? headInfo.get("totalActualAmount") : "");
+        // 定金
+        baseData.put("deposi", headInfo != null && headInfo.get("deposi") != null ? headInfo.get("deposi") : "");
+        // 时间
+        LocalDateTime newDate = LocalDateTime.now();
+        baseData.put("newDate", newDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
+
+
+        // 航向(1=宜昌-重庆,其他=重庆-宜昌)
         Integer direction = baseInfo != null && baseInfo.get("direction") != null ? (Integer) baseInfo.get("direction") : 0;
         baseData.put("direction", direction != null && direction == 1 ? "宜昌-重庆" : "重庆-宜昌");
+        // 如果为上水(宜昌 - 重庆)则航期往后推一天
+        baseData.put("travelDate", formatTravelDate(baseInfo, direction));
 
         // 总人数和国籍统计
         int totalCount = visitorList != null ? visitorList.size() : 0;
         StringBuilder countryStat = new StringBuilder();
 
         if (visitorList != null && !visitorList.isEmpty()) {
+            // 按国籍分组统计人数
             Map<String, Long> countryMap = visitorList.stream()
                     .filter(item -> ObjectUtil.isNotEmpty(item) && StringUtils.isNotEmpty(item.getNationalityName()))
                     .collect(Collectors.groupingBy(
@@ -2732,6 +2880,7 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
                             Collectors.counting()
                     ));
 
+            // 拼接国籍统计字符串(格式:数量/国籍)
             for (Map.Entry<String, Long> entry : countryMap.entrySet()) {
                 if (countryStat.length() > 0) {
                     countryStat.append(" ");
@@ -2742,41 +2891,251 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         baseData.put("totalCount", totalCount);
         baseData.put("countryStat", countryStat.toString());
 
-        // 5. 准备游客明细列表
+        // 6. 准备游客明细列表数据(按订单→房间→游客三级分组填充)
         List<Map<String, Object>> touristData = new ArrayList<>();
 
-        if (visitorList != null) {
-            for (int i = 0; i < visitorList.size(); i++) {
-                TouristExportVisitorVO visitor = visitorList.get(i);
-                Map<String, Object> item = new HashMap<>();
-                if (ObjectUtil.isEmpty(visitor)) {
-                    continue;
+        if (visitorList != null && !visitorList.isEmpty()) {
+            // 按订单号分组(使用 LinkedHashMap 保持 visitorList 的原始顺序)
+            Map<String, List<TouristExportVisitorVO>> orderGroupMap = visitorList.stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.toList()
+                    ));
+
+            // 先按 orderId 分组取每个订单的 payAmount(同一订单不重复计算)
+            // 再按 orderNo 分组累加 payAmount
+            Map<String, BigDecimal> orderPayAmountMap = visitorList.stream()
+                    .collect(Collectors.toMap(
+                            TouristExportVisitorVO::getOrderId,
+                            v -> v,
+                            (v1, v2) -> v1,  // orderId 相同时保留第一个
+                            LinkedHashMap::new
+                    ))
+                    .values().stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.reducing(
+                                    BigDecimal.ZERO,
+                                    v -> v.getPayAmount() != null ? v.getPayAmount() : BigDecimal.ZERO,
+                                    BigDecimal::add
+                            )
+                    ));
+
+
+            Map<String, BigDecimal> orderAmountMap = visitorList.stream()
+                    .collect(Collectors.toMap(
+                            TouristExportVisitorVO::getOrderId,
+                            v -> v,
+                            (v1, v2) -> v1,  // orderId 相同时保留第一个
+                            LinkedHashMap::new
+                    ))
+                    .values().stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.reducing(
+                                    BigDecimal.ZERO,
+                                    v -> v.getAmount() != null ? v.getAmount() : BigDecimal.ZERO,
+                                    BigDecimal::add
+                            )
+                    ));
+
+            // 定金
+            Map<String, BigDecimal> orderDeposiMap = visitorList.stream()
+                    .collect(Collectors.toMap(
+                            TouristExportVisitorVO::getOrderId,
+                            v -> v,
+                            (v1, v2) -> v1,  // orderId 相同时保留第一个
+                            LinkedHashMap::new
+                    ))
+                    .values().stream()
+                    .collect(Collectors.groupingBy(
+                            TouristExportVisitorVO::getOrderNo,
+                            LinkedHashMap::new,
+                            Collectors.reducing(
+                                    BigDecimal.ZERO,
+                                    v -> v.getDeposi() != null ? v.getDeposi() : BigDecimal.ZERO,
+                                    BigDecimal::add
+                            )
+                    ));
+
+            // 房间序号从1开始累加
+            int roomIndex = 1;
+            for (List<TouristExportVisitorVO> orderVisitors : orderGroupMap.values()) {
+                // 按房间索引分组(同一房间的游客合并显示房间详情)
+                // 使用 LinkedHashMap 保持房间在 visitorList 中的出现顺序
+                Map<String, List<TouristExportVisitorVO>> roomGroupMap = orderVisitors.stream()
+                        .collect(Collectors.groupingBy(
+                                v -> v.getRoomIndexId() != null ? v.getRoomIndexId() : "",
+                                LinkedHashMap::new,
+                                Collectors.toList()
+                        ));
+
+
+                for (List<TouristExportVisitorVO> roomVisitors : roomGroupMap.values()) {
+                    // 准备房间入住类型描述(如:2个成人/1个儿童)
+                    String roomDescription = prepareRoomDescription(roomVisitors);
+
+                    // 为该房间的每个游客创建一行数据
+                    for (TouristExportVisitorVO visitor : roomVisitors) {
+                        Map<String, Object> item = new HashMap<>();
+
+                        // 订单详情(8列):同一订单的游客合并显示这些列
+                        item.put("sourceName", StringUtils.isEmpty(visitor.getSourceName()) ? "" : visitor.getSourceName()); // 代理商名称
+                        item.put("orderNo", StringUtils.isEmpty(visitor.getOrderNo()) ? "" : visitor.getOrderNo()); // 订单号
+                        item.put("groupNo", StringUtils.isEmpty(visitor.getGroupNo()) ? "" : visitor.getGroupNo()); // 团号
+                        item.put("direction", StringUtils.isEmpty(visitor.getDirection()) ? "" : visitor.getDirection()); // 航向(宜昌-重庆/重庆-宜昌)
+
+                        item.put("travelDate", formatTravelDate(baseInfo, direction));
+                        item.put("amount", orderAmountMap.getOrDefault(visitor.getOrderNo(), BigDecimal.ZERO)); // 应收款
+
+
+                        // 实收款计算逻辑:根据支付状态判断
+                        //Integer payStatus = visitor.getPayStatus(); // 支付状态
+                        //BigDecimal payAmount = visitor.getPayAmount() != null ? visitor.getPayAmount() : BigDecimal.ZERO; // 实际金额
+                        BigDecimal deposi = visitor.getDeposi() != null ? visitor.getDeposi() : BigDecimal.ZERO; // 定金
+
+                        item.put("payAmount", orderPayAmountMap.getOrDefault(visitor.getOrderNo(), BigDecimal.ZERO)); // 实收款
+
+                        item.put("deposi", orderDeposiMap.getOrDefault(visitor.getOrderNo(), BigDecimal.ZERO)); // 定金
+
+                        // 房间详情(3列):同一房间的游客合并显示这些列
+                        item.put("floor", StringUtils.isEmpty(visitor.getFloor()) ? "" : visitor.getFloor()); // 楼层
+
+                        item.put("roomIndex", roomIndex); // 序号(按订单内的房间顺序)
+                        item.put("roomType", (StringUtils.isEmpty(visitor.getRoomType()) ? "" : visitor.getRoomType()) + (StringUtils.isEmpty(visitor.getFloor()) ? "" : (" (" + visitor.getFloor() + ")"))); // 房型(如:豪华标准间 (2F))
+                        item.put("roomDescription", roomDescription); // 入住类型(如:2个成人/1个儿童)
+
+                        // 游客详情(8列):每个游客独立显示
+                        item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName()); // 游客姓名
+                        item.put("gender", visitor.getGender() != null ? (visitor.getGender() == 1 ? "男" : "女") : ""); // 性别(1=男,其他=女)
+                        item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType())); // 证件类型(身份证/护照等)
+                        item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo()); // 证件号
+                        item.put("visitorType", StringUtils.isEmpty(visitor.getVisitorType()) ? "" : getPersonTypeAll(visitor.getVisitorType())); // 游客类型(成人/儿童/婴儿)
+                        item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName()); // 国籍(如:中国、美国)
+                        item.put("valueAddedService", StringUtils.isEmpty(visitor.getValueAddedService()) ? "" : formatPolicyName(visitor.getValueAddedService())); // 增值服务(如:接送站、保险等)
+                        item.put("policyName", ""); // 优惠政策(如:早鸟优惠、团立减等)
+                        item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark()); // 备注信息
+
+
+                        item.put("visitorHome", StringUtils.isEmpty(visitor.getVisitorType()) ? "" : DictFrameworkUtils.getDictDataLabel(DictTypeConstants.TOUR_TYPE, visitor.getVisitorType())); // 游客入住类型
+
+                        item.put("birthday", StringUtils.isEmpty(visitor.getBirthday()) ? "" : visitor.getBirthday()); // 生日
+                        item.put("mobile", StringUtils.isEmpty(visitor.getMobile()) ? "" : visitor.getMobile()); // 手机号
+                        item.put("jz", StringUtils.isEmpty(visitor.getJz()) ? "" : visitor.getJz()); // 是否接站
+                        item.put("orderStatus", StringUtils.isEmpty(visitor.getOrderStatus()) ? "" : TradeOrderStatusEnum.valueOf(Integer.valueOf(visitor.getOrderStatus())).getName()); // 订单状态
+                        touristData.add(item);
+                    }
+                    roomIndex++;
                 }
-                // 序号
-                item.put("xh", i + 1);
-                // 姓名
-                item.put("name", StringUtils.isEmpty(visitor.getName()) ? "" : visitor.getName());
-                // 国籍
-                item.put("nationalityName", StringUtils.isEmpty(visitor.getNationalityName()) ? "" : visitor.getNationalityName());
-                // 证件类型
-                item.put("credentialType", DictFrameworkUtils.getDictDataLabel(DictTypeConstants.VISITOR_CREDENTIAL_TYPE, visitor.getCredentialType()));
-                // 证件号
-                item.put("credentialNo", StringUtils.isEmpty(visitor.getCredentialNo()) ? "" : visitor.getCredentialNo());
-                // 出生年月
-                item.put("birthday", StringUtils.isEmpty(visitor.getBirthday()) ? "" : visitor.getBirthday());
-                // 备注
-                item.put("remark", StringUtils.isEmpty(visitor.getRemark()) ? "" : visitor.getRemark());
-
-                touristData.add(item);
-            }
-        }
-
-        // 6. 填充数据
-        excelWriter.fill(new FillWrapper("visitor", touristData),fillConfig, writeSheet);
+            }
+        }
+
+        // 7. 填充数据到Excel模板
+        //    先填充游客明细列表数据,再填充基础信息
+        excelWriter.fill(new FillWrapper("visitor", touristData), fillConfig, writeSheet);
         excelWriter.fill(baseData, writeSheet);
         excelWriter.finish();
 
         return new File(tmpFile);
     }
 
+    /**
+     * 格式化航期日期
+     *
+     * @param baseInfo 基础信息 Map
+     * @param direction 航向(1=宜昌 - 重庆,其他=重庆 - 宜昌)
+     * @return 格式化后的航期字符串(yyyy.M.d),上水时往前推一天
+     */
+    private String formatTravelDate(Map<String, Object> baseInfo, Integer direction) {
+        if (baseInfo == null || baseInfo.get("travelDate") == null) {
+            return "";
+        }
+
+        LocalDateTime travelDateTime = (LocalDateTime) baseInfo.get("travelDate");
+        // 上水(宜昌 - 重庆)航期往前推一天
+        if (direction != null && direction == 1) {
+            travelDateTime = travelDateTime.plusDays(-1);
+        }
+
+        return travelDateTime.format(DateTimeFormatter.ofPattern("yyyy.M.d"));
+    }
+
+    /**
+     * 准备房间入住类型描述
+     *
+     * 功能说明:
+     * 统计房间内各类型游客的数量,生成入住类型描述字符串
+     *
+     * 示例:
+     * - 2个成人 → "2个成人"
+     * - 1个成人/1个儿童 → "1个成人/1个儿童"
+     * - 2个成人/1个儿童/1个婴儿 → "2个成人/1个儿童/1个婴儿"
+     *
+     * @param roomVisitors 同一房间的游客列表
+     * @return 入住类型描述字符串
+     */
+    private String prepareRoomDescription(List<TouristExportVisitorVO> roomVisitors) {
+        if (roomVisitors == null || roomVisitors.isEmpty()) {
+            return "";
+        }
+
+        // 按游客类型统计数量
+        Map<String, Long> typeCountMap = roomVisitors.stream()
+                .filter(v -> StringUtils.isNotEmpty(v.getVisitorType()))
+                .collect(Collectors.groupingBy(TouristExportVisitorVO::getVisitorType, Collectors.counting()));
+
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, Long> entry : typeCountMap.entrySet()) {
+            if (sb.length() > 0) {
+                sb.append("/");
+            }
+            sb.append(entry.getValue()).append("个").append(getPersonTypeDes1(entry.getKey()));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 格式化优惠政策名称
+     *
+     * 功能说明:
+     * 将优惠政策字符串按 "、" 分割后,添加序号并用逗号连接
+     *
+     * 示例:
+     * - 输入:null 或 "" → 输出:""
+     * - 输入:"升舱礼遇2升3" → 输出:"①升舱礼遇2升3"
+     * - 输入:"升舱礼遇2升3、3升4、5升6" → 输出:"①升舱礼遇2升3"
+     *
+     * @param policyName 原始优惠政策字符串
+     * @return 格式化后的优惠政策字符串
+     */
+    private String formatPolicyName(String policyName) {
+        if (StringUtils.isEmpty(policyName)) {
+            return "";
+        }
+
+        // 按 "," 分割
+        String[] policies = policyName.split(",");
+        if (policies.length == 0) {
+            return "";
+        }
+
+        // 序号字符:①②③④⑤⑥⑦⑧⑨⑩...
+        String[] numberSymbols = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮"};
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < policies.length; i++) {
+            if (sb.length() > 0) {
+                sb.append("\n");
+            }
+            // 如果序号超过预定义范围,使用普通数字编号
+            String prefix = i < numberSymbols.length ? numberSymbols[i] : (i + 1) + ".";
+            sb.append(prefix).append(policies[i].trim());
+        }
+
+        return sb.toString();
+    }
 }

+ 7 - 1
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/pay/impl/TradeOrderPayServiceImpl.java

@@ -195,6 +195,7 @@ public class TradeOrderPayServiceImpl implements TradeOrderPayService {
                             //2.创建定金支付交易单
                             payOrderReqVO.setPayAmount(tradeOrder.getDeposi());
                             TradeOrderPayDO tradeOrderPayDO = buildOrderPay(tradeOrder, payOrderReqVO);
+                            tradeOrderPayDO.setOrderType(PAY_ORDER_TYPE_DEPOSI);
                             payOrderReqVO.setStoreId(tradeOrder.getStoreId());
                             tradeOrderRepositoryService.savePayOrder(tradeOrderPayDO);
                             payOrderReqVO.setAccountName(tradeOrder.getSourceName());
@@ -699,8 +700,13 @@ public class TradeOrderPayServiceImpl implements TradeOrderPayService {
     @TradeOrderLog(operateType = TradeOrderOperateTypeEnum.ORDER_DEPOSI_PAY)
     public void updateDeposiOrderPaid(TradeOrderDO tradeOrderDO, TradeOrderPayDO tradeOrderPayDO) {
         Integer orderStatus = tradeOrderDO.getOrderStatus();
+        BigDecimal realPayAmount = (tradeOrderDO.getRealPayAmount()==null?BigDecimal.ZERO:tradeOrderDO.getRealPayAmount()).add(tradeOrderDO.getRealPayAmount());
         tradeOrderMapper.update(new LambdaUpdateWrapper<TradeOrderDO>()
-                .eq(TradeOrderDO::getId, tradeOrderDO.getId()).set(TradeOrderDO::getOrderStatus, orderStatus).set(TradeOrderDO::getDeposiStatus, 2).set(TradeOrderDO::getDeposiPayTime, LocalDateTime.now()));
+                .eq(TradeOrderDO::getId, tradeOrderDO.getId())
+                .eq(TradeOrderDO::getRealPayAmount, realPayAmount)
+                .set(TradeOrderDO::getOrderStatus, orderStatus)
+                .set(TradeOrderDO::getDeposiStatus, 2)
+                .set(TradeOrderDO::getDeposiPayTime, LocalDateTime.now()));
         //保存订单日志
         TradeOrderLogUtils.setOrderInfo(tradeOrderDO.getId(), orderStatus, tradeOrderDO.getOrderStatus(), MapUtil.<String, Object>builder().put("payType", PayTypeEnum.valueOf(tradeOrderPayDO.getPaymentType()).getName()).put("payAmount", tradeOrderPayDO.getPayAmount()).build());
     }

+ 0 - 4
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/service/refund/impl/TradeRefundServiceImpl.java

@@ -739,10 +739,6 @@ public class TradeRefundServiceImpl implements TradeRefundService {
     private void validateBeforeRefund(TradeOrderDO tradeOrderDO, RefundDO refundDO) {
         //验证退款渠道
         validateRefundChannel(tradeOrderDO, refundDO);
-        //增加订单此时不能退款,等待合并后才能退
-        TradeOrderBindDO bindOrder = tradeOrderRepositoryService.getBindOrder(tradeOrderDO.getId(), TradeOrderBindEnum.ADD_ORDER.getType());
-        Asserts.isTrue(bindOrder == null, "订单{}为增加订单,需要等待订单合并后在主订单进行退款", tradeOrderDO.getOrderNo());
-
         int num = tradeOrderRepositoryService.getInvoiceOrderCount(tradeOrderDO.getOrderNo());
         Asserts.isTrue(num <= 0, "订单{}已经出开票,不能发起退款", tradeOrderDO.getOrderNo());
 

+ 36 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/InsuranceUtil.java

@@ -7,6 +7,7 @@ import com.yc.ship.framework.common.util.http.HttpUtils;
 import com.yc.ship.module.trade.api.insurance.dto.InsuranceApplyReqDTO;
 import com.yc.ship.module.trade.api.insurance.dto.InsuranceCancelReqDTO;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.http.*;
 import org.springframework.stereotype.Component;
 import org.springframework.util.LinkedMultiValueMap;
@@ -249,6 +250,9 @@ public class InsuranceUtil {
 
     public CommonResult queryInsurance(String orderNo) {
         try {
+            if(StringUtils.isBlank(orderNo)) {
+                return CommonResult.error(500, "订单号不能为空");
+            }
             Map<String, String> request = new LinkedHashMap<>();
             request.put("appId", APPID);
             request.put("externalOrderNo", orderNo);
@@ -263,6 +267,38 @@ public class InsuranceUtil {
             log.error("发送查询请求到阳光系统: {}", url);
             String s = HttpUtils.get(url, null);
             log.error("阳光系统查询响应内容: {}", s);
+            //{"phase":"INSURE","externalOrderNo":"tys-20261001-YC-14",
+            // "paiedAmount":"10.00","total":"1","insuredCount":"1","insureProcessingCount":"0",
+            // "cancelledCount":"0","cancelProcessingCount":"0","refundedAmount":"0.0",
+            // "cipher":"26e2e422d370653274ee74ef3edc7f93","policies":[{"externalPolicyNumber":"2037220248460828673",
+            // "service":"applyTeam","status":"SUCCESS","msg":"投保成功","policyNo":"HW61927008L7JB7J8Q00"}]}
+            return CommonResult.success(s);
+        } catch (Exception e) {
+            log.error("发送查询请求失败", e);
+            return CommonResult.error(500, "发送查询请求失败: " + e.getMessage());
+        }
+    }
+
+    public CommonResult queryInsuranceNew(String orderNo,Long orderId) {
+        try {
+            if(StringUtils.isBlank(orderNo)) {
+                return CommonResult.error(500, "订单号不能为空");
+            }
+            Map<String, String> request = new LinkedHashMap<>();
+            request.put("appId", APPID);
+            request.put("externalOrderNo", orderNo);
+            request.put("key", KEY);
+            String sign = MD5Util.md5(APPID + orderNo + KEY);
+
+            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+            params.add("appId", APPID);
+            params.add("externalOrderNo", orderNo);
+            params.add("sign", sign);
+            String url = HOST + QUERY_URL + "?appId=" + APPID + "&externalOrderNo=" + orderNo + "&sign=" + sign;
+            log.error(orderNo+"发送查询请求到阳光系统: {}", url);
+            String s = HttpUtils.get(url, null);
+            log.error(orderNo+"阳光系统查询响应内容: {}", s);
+
             return CommonResult.success(s);
         } catch (Exception e) {
             log.error("发送查询请求失败", e);

+ 196 - 3
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/utils/excel/ExcelStyleHandler.java

@@ -1,16 +1,20 @@
 package com.yc.ship.module.trade.utils.excel;
 
+
 import com.alibaba.excel.write.handler.CellWriteHandler;
 import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
+import com.yc.ship.module.trade.controller.admin.order.vo.order.TouristExportVisitorVO;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.RegionUtil;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public class ExcelStyleHandler implements CellWriteHandler {
-    private final int templateRowIndex;
+    private int templateRowIndex;
+
     private final Map<Integer, CellStyle> templateStyles = new HashMap<>();
 
     // 保存模板行的合并信息:列索引 -> [起始列, 结束列]
@@ -18,12 +22,39 @@ public class ExcelStyleHandler implements CellWriteHandler {
 
     private boolean templateProcessed = false;
 
+    /** 游客列表数据 */
+    private List<TouristExportVisitorVO> visitorList;
+
+    /** 数据起始行索引 */
+    private static final int DATA_START_ROW = 8;
+
+    /** 记录已处理的单元格数量 */
+    private int processedCellCount = 0;
+
+    /** 总数据行数 */
+    private int totalRows = 0;
+
+    private int fileType;
+
+    /** 每行数据列数(根据实际Excel模板列数调整) */
+    private static final int TOTAL_COLUMNS = 18;
+
+    private static final int TOTAL_COLUMNS2 = 16;
+    /** 是否已经执行过合并 */
+    private boolean mergeCompleted = false;
+
     public ExcelStyleHandler(int templateRowIndex) {
         this.templateRowIndex = templateRowIndex;
     }
 
-    @Override
-    public void afterCellDispose(CellWriteHandlerContext context) {
+    public ExcelStyleHandler(List<TouristExportVisitorVO> visitorList, int fileType) {
+        this.visitorList = visitorList;
+        this.fileType = fileType;
+        this.totalRows = visitorList != null ? visitorList.size() : 0;
+    }
+
+
+    public void afterCellDispose1(CellWriteHandlerContext context) {
         if (Boolean.TRUE.equals(context.getHead())) {
             return;
         }
@@ -151,4 +182,166 @@ public class ExcelStyleHandler implements CellWriteHandler {
             }
         }
     }
+
+    /**
+     * 单元格写入完成后执行合并
+     * 此方法在每个单元格写入完成后检查是否是最后一个单元格,如果是则统一处理合并
+     *
+     * @param context 单元格写入上下文
+     */
+    @Override
+    public void afterCellDispose(CellWriteHandlerContext context) {
+        // 只处理游客列表场景
+        if (visitorList == null || visitorList.isEmpty()) {
+            return;
+        }
+
+        // 跳过表头行
+        if (Boolean.TRUE.equals(context.getHead())) {
+            return;
+        }
+
+        // 统计处理的单元格数量
+        processedCellCount++;
+
+        int totalCells = 0;
+        // 当最后一个单元格写入完成后,执行合并
+        if(fileType == 1) {
+             totalCells = totalRows * TOTAL_COLUMNS;
+
+        } else {
+             totalCells = totalRows * TOTAL_COLUMNS2;
+        }
+        if (!mergeCompleted && processedCellCount >= totalCells) {
+            mergeCompleted = true;
+            performMerge(context.getWriteSheetHolder().getSheet());
+        }
+    }
+
+    /**
+     * 执行合并操作
+     * 此方法在数据写入完成后统一处理合并,避免逐行合并时的磁盘刷新问题
+     *
+     * 合并规则:
+     * 1. 第0列(代理商):按 sourceName 合并
+     * 2. 第1-7列(订单信息):按 orderNo 合并
+     * 3. 第8-10列(房间信息):按 roomIndexId 合并(同一订单内)
+     * 4. 横向不需要合并(按列单独合并)
+     *
+     * @param sheet Excel工作表
+     */
+    private void performMerge(Sheet sheet) {
+        // 遍历游客列表,从第二行开始与上一行比较
+        for (int i = 1; i < visitorList.size(); i++) {
+            TouristExportVisitorVO currentVisitor = visitorList.get(i);
+            TouristExportVisitorVO preVisitor = visitorList.get(i - 1);
+
+            int currentRowIndex = DATA_START_ROW + i;
+            int preRowIndex = DATA_START_ROW + i - 1;
+
+            // ===== 第0列(代理商):按 sourceName 合并 =====
+            if (currentVisitor.getSourceName() != null && preVisitor.getSourceName() != null
+                    && currentVisitor.getSourceName().equals(preVisitor.getSourceName())) {
+                mergeCell(sheet, currentRowIndex, preRowIndex, 1, 1);
+            }
+
+            // ===== 第1-7列(订单信息):按 orderNo 合并 =====
+            // 同一订单的所有游客合并显示订单信息
+            if (currentVisitor.getOrderNo() != null
+                    && currentVisitor.getOrderNo().equals(preVisitor.getOrderNo())) {
+                // 订单号列(第1列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 2, 2);
+                // 团号列(第2列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 3, 3);
+                // 航向列(第3列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 4, 4);
+                // 航期列(第4列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 5, 5);
+                // 应收款列(第5列)
+                mergeCell(sheet, currentRowIndex, preRowIndex, 6, 6);
+                if (fileType == 1) {
+                    // 实收款列(第6列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 7, 7);
+                    // 定金列(第7列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 8, 8);
+                }
+            }
+
+            // ===== 第8-10列(房间信息):按 roomIndexId 合并(同一订单内) =====
+            String currentRoomIndex = currentVisitor.getRoomIndexId() != null ? currentVisitor.getRoomIndexId() : "";
+            String preRoomIndex = preVisitor.getRoomIndexId() != null ? preVisitor.getRoomIndexId() : "";
+            if (currentRoomIndex.equals(preRoomIndex)
+                    && currentVisitor.getOrderNo() != null
+                    && currentVisitor.getOrderNo().equals(preVisitor.getOrderNo())) {
+                if (fileType == 1) {
+                    // 序号列(第8列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 9, 9);
+                    // 房型列(第9列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 10, 10);
+                    // 入住类型列(第10列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 11, 11);
+                } else {
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 7, 7);
+                    // 房型列(第9列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 8, 8);
+                    // 入住类型列(第10列)
+                    mergeCell(sheet, currentRowIndex, preRowIndex, 9, 9);
+                }
+            }
+        }
+    }
+
+    /**
+     * 合并单元格并设置边框
+     *
+     * 功能说明:
+     * - 如果目标单元格已存在合并区域,则扩展合并区域
+     * - 如果目标单元格未合并,则创建新的合并区域
+     * - 设置合并单元格的边框样式
+     *
+     * @param sheet Excel工作表
+     * @param currentRow 当前行索引
+     * @param preRow 上一行索引
+     * @param startCol 起始列索引
+     * @param endCol 结束列索引
+     */
+    private void mergeCell(Sheet sheet, int currentRow, int preRow, int startCol, int endCol) {
+        List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
+
+        // 检查上一行是否已存在合并区域
+        for (int i = 0; i < mergedRegions.size(); i++) {
+            CellRangeAddress region = mergedRegions.get(i);
+            // 如果合并区域包含上一行的指定列
+            if (region.getFirstRow() <= preRow && region.getLastRow() >= preRow
+                    && region.getFirstColumn() == startCol && region.getLastColumn() == endCol) {
+                // 扩展合并区域
+                sheet.removeMergedRegion(i);
+                CellRangeAddress newRegion = new CellRangeAddress(region.getFirstRow(), currentRow, startCol, endCol);
+                sheet.addMergedRegion(newRegion);
+                // setBorder(newRegion, sheet);
+                return;
+            }
+        }
+
+        // 如果没有找到已存在的合并区域,创建新的合并区域
+        CellRangeAddress newRegion = new CellRangeAddress(preRow, currentRow, startCol, endCol);
+        sheet.addMergedRegion(newRegion);
+        // setBorder(newRegion, sheet);
+    }
+
+    /**
+     * 设置合并单元格边框
+     *
+     * 功能说明:
+     * 为合并区域设置细边框样式,使合并后的单元格显示边框
+     *
+     * @param cra 合并区域对象
+     * @param sheet Excel工作表
+     */
+    private void setBorder(CellRangeAddress cra, Sheet sheet) {
+        RegionUtil.setBorderBottom(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderLeft(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderRight(BorderStyle.THIN, cra, sheet);
+        RegionUtil.setBorderTop(BorderStyle.THIN, cra, sheet);
+    }
 }

+ 4 - 2
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeOrderMapper.xml

@@ -199,6 +199,7 @@
             SUM(a.num4) AS leaderNum,
             SUM(a.payAmount) AS totalPayAmount,
             SUM(a.actual_amount) AS totalActualAmount,
+            SUM(a.deposi) as deposi,
             IFNULL((
                 SELECT GROUP_CONCAT(CONCAT( nationalityName, '(', num, ')') SEPARATOR ' ')
                 FROM (
@@ -271,6 +272,7 @@
             SUM(CASE WHEN tv.type = 'with' THEN 1 ELSE 0 END) AS num3,
             SUM(CASE WHEN tv.type = 'leader' THEN 1 ELSE 0 END) AS num4,
             td.pay_amount AS payAmount,
+            td.deposi,
             IFNULL(topay.actual_amount, 0) AS actual_amount
             FROM trade_order td
             INNER JOIN trade_order_user tou ON td.id = tou.order_id AND tou.deleted = 0
@@ -2429,10 +2431,10 @@
             </foreach>
         </if>
         <if test="vo.contactName != null and vo.contactName != ''">
-            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+            AND tor.link_man LIKE CONCAT('%', #{vo.contactName}, '%')
         </if>
         <if test="vo.mobile != null and vo.mobile != ''">
-            AND tor.mobile = #{vo.mobile}
+            AND tor.link_mobile = #{vo.mobile}
         </if>
         LIMIT 1
     </select>

+ 35 - 11
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeVisitorMapper.xml

@@ -248,19 +248,42 @@
     <!-- 查询游客名单导出游客列表 -->
     <select id="selectTouristExportVisitor" resultType="com.yc.ship.module.trade.controller.admin.order.vo.order.TouristExportVisitorVO">
         SELECT
-        tv.name,
-        a.name as nationalityName,
-        tv.credential_type as credentialType,
-        tv.credential_no as credentialNo,
-        tv.birthday,
-        tor.remark
+            od.name AS sourceName,
+            tor.order_no AS orderNo,
+            tor.group_no AS groupNo,
+            CASE WHEN rr.direction = 1 THEN '上水' ELSE '下水' END AS direction,
+            CASE WHEN tjz.is_jz = 1 THEN '是' ELSE '否' END AS jz,
+            DATE_FORMAT(pv.start_time, '%Y.%m.%d') AS travelDate,
+            tor.pay_amount AS amount,
+            top.pay_amount AS payAmount,
+            tor.deposi,
+            tv.room_index_id AS roomIndexId,
+            torm.room_model_name AS roomType,
+            tv.name,
+            tv.gender,
+            tv.birthday,
+            tv.mobile,
+            tv.order_id as orderId,
+            tv.credential_type AS credentialType,
+            tv.credential_no AS credentialNo,
+            tv.type AS visitorType,
+            tv.floor as floor,
+            a.name AS nationalityName,
+            GROUP_CONCAT(ps.product_name) AS valueAddedService,
+            tor.remark,
+            tor.order_status as orderStatus
         FROM trade_visitor tv
         INNER JOIN trade_order tor ON tv.order_id = tor.id AND tor.deleted = 0
-        INNER JOIN trade_detail td ON tv.detail_id = td.id AND td.deleted = 0 AND td.product_type = 0
+        INNER JOIN trade_detail td ON tv.id = td.visitor_id AND td.deleted = 0
         LEFT JOIN area a ON tv.nationality = a.id
         LEFT JOIN product_voyage pv ON tor.voyage_id = pv.id
-        LEFT JOIN ota_distributor od on od.id = tor.source_id
+        LEFT JOIN resource_route rr ON pv.route_id = rr.id
+        LEFT JOIN ota_distributor od ON tor.source_id = od.id
         LEFT JOIN trade_order_room_model torm ON tv.room_index_id = torm.room_index_id AND torm.deleted = 0
+        left JOIN product_spu ps on ps.id = td.product_id
+        left join trade_order_pay top ON top.order_id = tor.id and top.deleted = 0 and top.pay_status = 1
+        left Join trade_order_jz tjz on tjz.order_id = tor.id
+
         WHERE tv.deleted = 0
 
             <if test="vo.orderNo != null and vo.orderNo != ''">
@@ -318,12 +341,13 @@
             </foreach>
         </if>
         <if test="vo.contactName != null and vo.contactName != ''">
-            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+            AND tor.link_man LIKE CONCAT('%', #{vo.contactName}, '%')
         </if>
         <if test="vo.mobile != null and vo.mobile != ''">
-            AND tor.mobile = #{vo.mobile}
+            AND tor.link_mobile = #{vo.mobile}
         </if>
-        ORDER BY tor.order_no ASC, tv.id ASC
+        group by tv.id
+        ORDER BY od.id ASC,tor.order_no ASC, tv.room_index_id ASC
     </select>
     <select id="selectPersonDesc" resultType="com.yc.ship.module.trade.controller.app.otc.vo.AppPersonDescVO">
         select order_id,name, count(1) num

BIN
ship-module-trade/ship-module-trade-biz/src/main/resources/templates/tourist_template.xlsx


BIN
ship-module-trade/ship-module-trade-biz/src/main/resources/templates/tourist_template_operator.xlsx