Prechádzať zdrojové kódy

订单管理—导出游客接口新增

jinch 3 týždňov pred
rodič
commit
6c996c213c

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

@@ -620,4 +620,19 @@ public class OtcTradeOrderController {
     public CommonResult<OrderTotalRespVO> getOrderTotal(@Valid @RequestBody OrderTotalQueryVO queryVO) {
         return success(otcTradeOrderService.getOrderTotal(queryVO));
     }
+
+    /**
+     * 导出游客名单
+     */
+    @GetMapping("/export-touristExcel")
+    @Operation(summary = "导出游客名单 Excel")
+    @OperateLog(type = EXPORT, enable = false)
+    @PlatTenantEnv
+    public void exportTouristList(@Valid TradeOrderPageReqVO pageReqVO, HttpServletResponse response) throws IOException {
+        File tempFile = otcTradeOrderService.exportTouristList(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());
+    }
 }

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

@@ -0,0 +1,30 @@
+package com.yc.ship.module.trade.controller.admin.order.vo.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 游客名单导出 - 游客信息VO
+ */
+@Schema(description = "游客名单导出 - 游客信息")
+@Data
+public class TouristExportVisitorVO {
+
+    @Schema(description = "游客姓名")
+    private String name;
+
+    @Schema(description = "国籍名称")
+    private String nationalityName;
+
+    @Schema(description = "证件类型")
+    private Integer credentialType;
+
+    @Schema(description = "证件号")
+    private String credentialNo;
+
+    @Schema(description = "生日")
+    private String birthday;
+
+    @Schema(description = "备注")
+    private String remark;
+}

+ 5 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeOrderMapper.java

@@ -188,4 +188,9 @@ public interface TradeOrderMapper extends BaseMapperX<TradeOrderDO> {
     Map<String, Object> getOrderCount(@Param("vo") TradeOrderPageReqVO pageReqVO);
 
     OrderTotalRespVO selectTotalByOrderIds(@Param("orderIds") List<Long> orderIds);
+
+    /**
+     * 查询游客名单导出基础信息(船名、航期、航向)
+     */
+    Map<String, Object> selectTouristExportBase(@Param("vo") TradeOrderPageReqVO reqVO);
 }

+ 6 - 0
ship-module-trade/ship-module-trade-biz/src/main/java/com/yc/ship/module/trade/dal/mysql/order/TradeVisitorMapper.java

@@ -5,6 +5,7 @@ import com.yc.ship.framework.common.pojo.PageResult;
 import com.yc.ship.framework.mybatis.core.mapper.BaseMapperX;
 import com.yc.ship.framework.tenant.core.aop.TenantIgnore;
 import com.yc.ship.module.product.api.dto.OrderRoomUseDTO;
+import com.yc.ship.module.trade.controller.admin.order.vo.order.TouristExportVisitorVO;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.TradeOrderPageReqVO;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.TradeOrderRespExcelVO;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.TradeVisitorRespVO;
@@ -79,4 +80,9 @@ public interface TradeVisitorMapper extends BaseMapperX<TradeVisitorDO> {
     List<TradeVisitorDO> selectListByVoyageIdAndRoomId(@Param("voyageId")Long voyageId, @Param("roomId")Long roomId);
 
     List<TradeOrderRespExcelVO> getExportVisitorList(@Param("vo") TradeOrderPageReqVO reqVO);
+
+    /**
+     * 查询游客名单导出游客列表
+     */
+    List<TouristExportVisitorVO> selectTouristExportVisitor(@Param("vo") TradeOrderPageReqVO reqVO);
 }

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

@@ -10,6 +10,7 @@ import com.yc.ship.module.trade.api.dto.TradeOrderRespDTO;
 import com.yc.ship.module.trade.controller.admin.order.vo.order.*;
 import com.yc.ship.module.trade.dal.dataobject.order.TradeOrderDO;
 
+import java.io.File;
 import java.util.List;
 
 /**
@@ -57,7 +58,6 @@ public interface TradeOrderService {
      */
     CommonResult<Long> bindOrder(TradeBindOrderReqDTO tradeBindOrderReqDTO);
 
-
     /**
      * 修改游客信息
      *

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

@@ -149,4 +149,9 @@ public interface OtcTradeOrderService {
      * 获取订单统计信息
      */
     OrderTotalRespVO getOrderTotal(OrderTotalQueryVO queryVO);
+
+    /**
+     * 导出游客名单
+     */
+    File exportTouristList(TradeOrderPageReqVO pageReqVO);
 }

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

@@ -107,6 +107,7 @@ import com.yc.ship.module.trade.service.otc.OtcTradeOrderService;
 import com.yc.ship.module.trade.service.pay.TradeOrderPayService;
 import com.yc.ship.module.trade.service.refund.TradeRefundRepositoryService;
 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.ss.usermodel.*;
@@ -2489,4 +2490,100 @@ public class OtcTradeOrderServiceImpl implements OtcTradeOrderService {
         );
         return CommonResult.success("修改成功");
     }
+
+    @Override
+    public File exportTouristList(TradeOrderPageReqVO reqVO) {
+        // 1. 查询订单基础信息(船名、航期、航向)
+        Map<String, Object> baseInfo = tradeOrderMapper.selectTouristExportBase(reqVO);
+
+        // 2. 查询游客列表
+        List<TouristExportVisitorVO> visitorList = tradeVisitorMapper.selectTouristExportVisitor(reqVO);
+
+        // 3. 加载模板
+        InputStream template = getClass().getClassLoader().getResourceAsStream("templates/tourist_template.xlsx");
+        String fileName =  String.valueOf(System.currentTimeMillis());
+        String tmpFile = "/tmp/" + fileName + "_tourist.xlsx";
+        //String tmpFile = "D:/tmp/" + fileName + "_tourist.xlsx";
+
+        // 先不合并,直接导出
+        ExcelWriter excelWriter = EasyExcel.write(tmpFile).withTemplate(template).build();
+        WriteSheet writeSheet = EasyExcelFactory.writerSheet().registerWriteHandler(new ExcelStyleHandler(9)).build();
+        FillConfig fillConfig = FillConfig.builder().forceNewRow(true).autoStyle(true).build();
+
+        // 4. 准备基础信息数据
+        Map<String, Object> baseData = new HashMap<>();
+
+        // 船名
+        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"))
+                : "");
+
+
+        // 航向
+        Integer direction = baseInfo != null && baseInfo.get("direction") != null ? (Integer) baseInfo.get("direction") : 0;
+        baseData.put("direction", direction != null && direction == 1 ? "宜昌-重庆" : "重庆-宜昌");
+
+        // 总人数和国籍统计
+        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(
+                            TouristExportVisitorVO::getNationalityName,
+                            Collectors.counting()
+                    ));
+
+            for (Map.Entry<String, Long> entry : countryMap.entrySet()) {
+                if (countryStat.length() > 0) {
+                    countryStat.append(" ");
+                }
+                countryStat.append(entry.getValue()).append(" / ").append(entry.getKey());
+            }
+        }
+        baseData.put("totalCount", totalCount);
+        baseData.put("countryStat", countryStat.toString());
+
+        // 5. 准备游客明细列表
+        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;
+                }
+                // 序号
+                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);
+        excelWriter.fill(baseData, writeSheet);
+        excelWriter.finish();
+
+        return new File(tmpFile);
+    }
+
 }

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

@@ -0,0 +1,154 @@
+package com.yc.ship.module.trade.utils.excel;
+
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ExcelStyleHandler implements CellWriteHandler {
+    private final int templateRowIndex;
+    private final Map<Integer, CellStyle> templateStyles = new HashMap<>();
+
+    // 保存模板行的合并信息:列索引 -> [起始列, 结束列]
+    private final Map<Integer, int[]> templateMergeInfo = new HashMap<>();
+
+    private boolean templateProcessed = false;
+
+    public ExcelStyleHandler(int templateRowIndex) {
+        this.templateRowIndex = templateRowIndex;
+    }
+
+    @Override
+    public void afterCellDispose(CellWriteHandlerContext context) {
+        if (Boolean.TRUE.equals(context.getHead())) {
+            return;
+        }
+
+        Cell cell = context.getCell();
+        Row currentRow = cell.getRow();
+        int currentRowIndex = currentRow.getRowNum();
+        int colIndex = cell.getColumnIndex();
+        Sheet sheet = context.getWriteSheetHolder().getSheet();
+        Workbook workbook = sheet.getWorkbook();
+
+        // 处理模板行:保存样式和合并信息
+        if (currentRowIndex == templateRowIndex) {
+            if (!templateProcessed) {
+                // 保存样式
+                saveTemplateStyles(currentRow, workbook);
+
+                // 保存合并信息
+                saveTemplateMergeInfo(sheet);
+
+                templateProcessed = true;
+            }
+            return;
+        }
+
+        // 跳过模板行之前的行
+        if (currentRowIndex < templateRowIndex) {
+            return;
+        }
+
+        // ===== 为所有单元格设置样式(包括合并区域内的)=====
+       // CellStyle newStyle = workbook.createCellStyle();
+
+// 应用样式
+        if (templateStyles.containsKey(colIndex)) {
+            //newStyle.cloneStyleFrom(templateStyles.get(colIndex));
+
+            cell.setCellStyle(templateStyles.get(colIndex));
+        }
+
+
+        // 如果这个列在合并区域内,设置边框
+        /*if (templateMergeInfo.containsKey(colIndex)) {
+            int[] range = templateMergeInfo.get(colIndex);
+            int firstCol = range[0];
+            int lastCol = range[1];
+
+            // 根据在合并区域中的位置设置边框
+            setMergeCellBorders(newStyle, colIndex, firstCol, lastCol);
+        }*/
+
+       // cell.setCellStyle(newStyle);
+
+        // 应用合并(只处理合并区域的第一个列)
+        if (templateMergeInfo.containsKey(colIndex)) {
+            int[] range = templateMergeInfo.get(colIndex);
+            int firstCol = range[0];
+            int lastCol = range[1];
+
+            // 只在当前列是合并区域的第一个列时创建合并
+            if (colIndex == firstCol) {
+                CellRangeAddress cra = new CellRangeAddress(
+                        currentRowIndex, currentRowIndex, firstCol, lastCol
+                );
+                sheet.addMergedRegion(cra);
+
+                // 为合并区域设置完整边框
+                setMergeCellBorders(sheet.getColumnStyle(colIndex), colIndex, firstCol, lastCol);
+
+            }
+
+        }
+
+
+
+
+    }
+
+
+    /**
+     * 为合并区域的单元格设置边框
+     * @param style 单元格样式
+     * @param colIndex 当前列索引
+     * @param firstCol 合并区域起始列
+     * @param lastCol 合并区域结束列
+     */
+    private void setMergeCellBorders(CellStyle style,  int colIndex, int firstCol, int lastCol) {
+        // 设置所有边框为细线
+        style.setBorderTop(BorderStyle.THIN);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setBorderRight(BorderStyle.THIN);
+
+        // 如果有特殊需求,可以根据位置设置不同边框:
+        // - 合并区域的第一个单元格:左、上、下边框
+        // - 合并区域的最后一个单元格:右、上、下边框
+        // - 中间的单元格:上、下边框
+    }
+
+    /**
+     * 保存模板行的合并信息
+     */
+    private void saveTemplateMergeInfo(Sheet sheet) {
+        List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
+        for (CellRangeAddress cra : mergedRegions) {
+            // 只处理模板行上的横向合并
+            if (cra.getFirstRow() == templateRowIndex && cra.getLastRow() == templateRowIndex) {
+                int firstCol = cra.getFirstColumn();
+                int lastCol = cra.getLastColumn();
+
+                // 记录这个合并区域涉及的所有列
+                for (int col = firstCol; col <= lastCol; col++) {
+                    templateMergeInfo.put(col, new int[]{firstCol, lastCol});
+                }
+            }
+        }
+    }
+
+    private void saveTemplateStyles(Row templateRow, Workbook workbook) {
+        for (Cell cell : templateRow) {
+            if (cell != null && cell.getCellStyle() != null) {
+                CellStyle newStyle = workbook.createCellStyle();
+                newStyle.cloneStyleFrom(cell.getCellStyle());
+                templateStyles.put(cell.getColumnIndex(), newStyle);
+            }
+        }
+    }
+}

+ 72 - 0
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeOrderMapper.xml

@@ -2105,5 +2105,77 @@
             #{item}
         </foreach>
     </select>
+
+
+    <!-- 查询游客名单导出基础信息(船名、航期、航向) -->
+    <select id="selectTouristExportBase" resultType="java.util.Map">
+        SELECT
+        rs.name as shipName,
+        pv.start_time as travelDate,
+        rr.direction as direction
+        FROM trade_order tor
+        LEFT JOIN product_voyage pv ON tor.voyage_id = pv.id
+        LEFT JOIN resource_ship rs ON pv.ship_id = rs.id
+        LEFT JOIN resource_route rr ON pv.route_id = rr.id
+        LEFT JOIN ota_distributor od on od.id = tor.source_id
+        WHERE tor.deleted = 0
+        <choose>
+            <when test="vo.orderNoList != null and vo.orderNoList.size() > 0">
+                AND tor.order_no IN
+                <foreach collection="vo.orderNoList" item="item" separator="," open="(" close=")">
+                    #{item}
+                </foreach>
+            </when>
+            <when test="vo.orderNo != null and vo.orderNo != ''">
+                AND tor.order_no = #{vo.orderNo}
+            </when>
+        </choose>
+        <if test="vo.voyageId != null and vo.voyageId != ''">
+            AND tor.voyage_id = #{vo.voyageId}
+        </if>
+        <if test="vo.shipId != null and vo.shipId != ''">
+            AND pv.ship_id = #{vo.shipId}
+        </if>
+        <if test="vo.routeId != null and vo.routeId != ''">
+            AND pv.route_id = #{vo.routeId}
+        </if>
+        <if test="vo.travelDateStart != null and vo.travelDateStart != ''">
+            AND pv.start_time &gt;= #{vo.travelDateStart}
+        </if>
+        <if test="vo.travelDateEnd != null and vo.travelDateEnd != ''">
+            AND pv.start_time &lt;= #{vo.travelDateEnd}
+        </if>
+        <if test="vo.orderDateStart != null and vo.orderDateStart != ''">
+            AND tor.create_time &gt;= #{vo.orderDateStart}
+        </if>
+        <if test="vo.orderDateEnd != null and vo.orderDateEnd != ''">
+            AND tor.create_time &lt;= #{vo.orderDateEnd}
+        </if>
+        <if test="vo.orderStatus != null and vo.orderStatus.size() > 0">
+            AND tor.order_status IN
+            <foreach collection="vo.orderStatus" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        <if test="vo.otaId != null and vo.otaId != ''">
+            AND tor.source_id = #{vo.otaId}
+        </if>
+        <if test="vo.otaCateId != null and vo.otaCateId != ''">
+            AND od.ota_category_id = #{vo.otaCateId}
+        </if>
+        <if test="vo.otaCateIds != null and vo.otaCateIds.size() > 0">
+            AND od.ota_category_id IN
+            <foreach collection="vo.otaCateIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        <if test="vo.contactName != null and vo.contactName != ''">
+            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+        </if>
+        <if test="vo.mobile != null and vo.mobile != ''">
+            AND tor.mobile = #{vo.mobile}
+        </if>
+        LIMIT 1
+    </select>
 </mapper>
 

+ 89 - 0
ship-module-trade/ship-module-trade-biz/src/main/resources/mapper/order/TradeVisitorMapper.xml

@@ -243,4 +243,93 @@
             </foreach>
         </if>
     </select>
+
+
+    <!-- 查询游客名单导出游客列表 -->
+    <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
+        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
+        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 trade_order_room_model torm ON tv.room_index_id = torm.room_index_id AND torm.deleted = 0
+        WHERE tv.deleted = 0
+        <choose>
+            <when test="vo.orderNoList != null and vo.orderNoList.size() > 0">
+                AND tor.order_no IN
+                <foreach collection="vo.orderNoList" item="item" separator="," open="(" close=")">
+                    #{item}
+                </foreach>
+            </when>
+            <when test="vo.orderNo != null and vo.orderNo != ''">
+                AND tor.order_no = #{vo.orderNo}
+            </when>
+        </choose>
+        <if test="vo.voyageId != null and vo.voyageId != ''">
+            AND tor.voyage_id = #{vo.voyageId}
+        </if>
+        <if test="vo.shipId != null and vo.shipId != ''">
+            AND pv.ship_id = #{vo.shipId}
+        </if>
+        <if test="vo.routeId != null and vo.routeId != ''">
+            AND pv.route_id = #{vo.routeId}
+        </if>
+        <if test="vo.travelDateStart != null and vo.travelDateStart != ''">
+            AND pv.start_time &gt;= #{vo.travelDateStart}
+        </if>
+        <if test="vo.travelDateEnd != null and vo.travelDateEnd != ''">
+            AND pv.start_time &lt;= #{vo.travelDateEnd}
+        </if>
+        <if test="vo.orderDateStart != null and vo.orderDateStart != ''">
+            AND tor.create_time &gt;= #{vo.orderDateStart}
+        </if>
+        <if test="vo.orderDateEnd != null and vo.orderDateEnd != ''">
+            AND tor.create_time &lt;= #{vo.orderDateEnd}
+        </if>
+        <if test="vo.orderStatus != null and vo.orderStatus.size() > 0">
+            AND tor.order_status IN
+            <foreach collection="vo.orderStatus" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        <if test="vo.userName != null and vo.userName != ''">
+            AND tv.name LIKE CONCAT('%', #{vo.userName}, '%')
+        </if>
+        <if test="vo.credentialNo != null and vo.credentialNo != ''">
+            AND tv.credential_no = #{vo.credentialNo}
+        </if>
+        <if test="vo.roomModelId != null and vo.roomModelId != ''">
+            AND torm.room_model_id = #{vo.roomModelId}
+        </if>
+        <if test="vo.floor != null and vo.floor != ''">
+            AND tv.floor = #{vo.floor}
+        </if>
+        <if test="vo.otaId != null and vo.otaId != ''">
+            AND tor.source_id = #{vo.otaId}
+        </if>
+        <if test="vo.otaCateId != null and vo.otaCateId != ''">
+            AND od.ota_category_id = #{vo.otaCateId}
+        </if>
+        <if test="vo.otaCateIds != null and vo.otaCateIds.size() > 0">
+            AND od.ota_category_id IN
+            <foreach collection="vo.otaCateIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        <if test="vo.contactName != null and vo.contactName != ''">
+            AND tor.contact_name LIKE CONCAT('%', #{vo.contactName}, '%')
+        </if>
+        <if test="vo.mobile != null and vo.mobile != ''">
+            AND tor.mobile = #{vo.mobile}
+        </if>
+        ORDER BY tor.order_no ASC, tv.id ASC
+    </select>
 </mapper>

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