提交发票修改流程

This commit is contained in:
2026-03-02 08:23:19 +08:00
parent 1be18e2ff5
commit 572e9a443f
133 changed files with 1599 additions and 44 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -68,5 +68,30 @@
<version>2.2.0-jdk8-snapshot</version>
<scope>compile</scope>
</dependency>
<!-- Thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- OpenHTMLToPDF - HTML转PDF -->
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-svg-support</artifactId>
<version>1.0.10</version>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra-biz</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@@ -30,7 +30,7 @@ public class TireLoginRespVO{
@Schema(description = "门店ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11176")
@ExcelProperty("门店ID")
private Long storeId;
private String storeId;
@Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11178")
@ExcelProperty("仓库编号")

View File

@@ -1,9 +1,15 @@
package cn.iocoder.yudao.module.car.controller.admin.renewalorder;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import com.google.common.collect.Lists;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
@@ -18,6 +24,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.enums.UserTypeEnum.ADMIN;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -53,6 +61,22 @@ public class RenewalOrderController {
return success(true);
}
@PostMapping("/generate-contract")
@Operation(summary = "生成在线合同 PDF 并上传(回写 contractUrl")
@Parameter(name = "id", description = "订单编号", required = true)
@PreAuthorize("@ss.hasPermission('car:renewal-order:update')")
public CommonResult<String> generateContract(@RequestParam("id") Long id) {
return success(renewalOrderService.generateContract(id));
}
@GetMapping("/generate-contract-html")
@Operation(summary = "生成合同 HTML用于预览")
@Parameter(name = "id", description = "订单编号", required = true)
@PreAuthorize("@ss.hasPermission('car:renewal-order:query')")
public CommonResult<String> generateContractHtml(@RequestParam("id") Long id) {
return success(renewalOrderService.generateContractHtml(id));
}
@DeleteMapping("/delete")
@Operation(summary = "删除车辆续保订单")
@Parameter(name = "id", description = "编号", required = true)
@@ -70,15 +94,28 @@ public class RenewalOrderController {
RenewalOrderDO renewalOrder = renewalOrderService.getRenewalOrder(id);
return success(BeanUtils.toBean(renewalOrder, RenewalOrderRespVO.class));
}
@Resource
private PermissionService permissionService;
@GetMapping("/page")
@Operation(summary = "获得车辆续保订单分页")
@PreAuthorize("@ss.hasPermission('car:renewal-order:query')")
public CommonResult<PageResult<RenewalOrderRespVO>> getRenewalOrderPage(@Valid RenewalOrderPageReqVO pageReqVO) {
public CommonResult<PageResult<RenewalOrderRespVO>> getRenewalOrderPage(
@Valid RenewalOrderPageReqVO pageReqVO) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
boolean isTenantAdmin = permissionService.hasAnyRoles(userId, "tenant_admin");
if (!isTenantAdmin) {
pageReqVO.setCreator(String.valueOf(userId));
}
PageResult<RenewalOrderDO> pageResult = renewalOrderService.getRenewalOrderPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, RenewalOrderRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出车辆续保订单 Excel")
@PreAuthorize("@ss.hasPermission('car:renewal-order:export')")

View File

@@ -96,15 +96,48 @@ public class RenewalOrderPageReqVO extends PageParam {
@Schema(description = "合同备注", example = "你说的对")
private String contractRemark;
private String contractUrl;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
private String creator;
private Long storeId;
private String storeName;
private String invoiceUrl;
private String productType;
@Schema(description = "客户签名")
private String customerSignatureUrl;
@Schema(description = "身份证正面")
private String idCardFrontUrl;
@Schema(description = "身份证反面")
private String idCardBackUrl;
@Schema(description = "行驶证")
private String drivingLicenseUrl;
@Schema(description = "购车发票多张JSON字符串")
private List<String> carInvoiceUrls;
@Schema(description = "购置税发票多张JSON字符串")
private List<String> purchaseTaxInvoiceUrls;
@Schema(description = "商业险保单多张JSON字符串")
private List<String> businessInsurancePolicyUrls;
@Schema(description = "合格证")
private String certificateOfConformityUrl;
@Schema(description = "里程表照片")
private String odometerPhotoUrl;
@Schema(description = "车名牌照片")
private String nameplatePhotoUrl;
}

View File

@@ -122,6 +122,7 @@ public class RenewalOrderRespVO {
@Schema(description = "合同备注", example = "你说的对")
@ExcelProperty("合同备注")
private String contractRemark;
private String contractUrl;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
@@ -132,5 +133,45 @@ public class RenewalOrderRespVO {
private String storeName;
private String invoiceUrl;
private String productType;
@Schema(description = "客户签名")
@ExcelProperty("客户签名")
private String customerSignatureUrl;
@Schema(description = "身份证正面")
@ExcelProperty("身份证正面")
private String idCardFrontUrl;
@Schema(description = "身份证反面")
@ExcelProperty("身份证反面")
private String idCardBackUrl;
@Schema(description = "行驶证")
@ExcelProperty("行驶证")
private String drivingLicenseUrl;
@Schema(description = "购车发票(多张)")
@ExcelProperty("购车发票")
private List<String> carInvoiceUrls;
@Schema(description = "购置税发票(多张)")
@ExcelProperty("购置税发票")
private List<String> purchaseTaxInvoiceUrls;
@Schema(description = "商业险保单(多张)")
@ExcelProperty("商业险保单")
private List<String> businessInsurancePolicyUrls;
@Schema(description = "合格证")
@ExcelProperty("合格证")
private String certificateOfConformityUrl;
@Schema(description = "里程表照片")
@ExcelProperty("里程表照片")
private String odometerPhotoUrl;
@Schema(description = "车名牌照片")
@ExcelProperty("车名牌照片")
private String nameplatePhotoUrl;
}

View File

@@ -94,6 +94,7 @@ public class RenewalOrderSaveReqVO {
@Schema(description = "合同备注", example = "你说的对")
private String contractRemark;
private String contractUrl;
private Long storeId;
@@ -101,4 +102,35 @@ public class RenewalOrderSaveReqVO {
private String invoiceUrl;
private String productType;
@Schema(description = "客户签名")
private String customerSignatureUrl;
@Schema(description = "身份证正面")
private String idCardFrontUrl;
@Schema(description = "身份证反面")
private String idCardBackUrl;
@Schema(description = "行驶证")
private String drivingLicenseUrl;
@Schema(description = "购车发票(多张)")
private List<String> carInvoiceUrls;
@Schema(description = "购置税发票(多张)")
private List<String> purchaseTaxInvoiceUrls;
@Schema(description = "商业险保单(多张)")
private List<String> businessInsurancePolicyUrls;
@Schema(description = "合格证")
private String certificateOfConformityUrl;
@Schema(description = "里程表照片")
private String odometerPhotoUrl;
@Schema(description = "车名牌照片")
private String nameplatePhotoUrl;
}

View File

@@ -79,6 +79,17 @@ public class RenewalProductController {
return success(BeanUtils.toBean(pageResult, RenewalProductRespVO.class));
}
@GetMapping("/findByProductType")
@Operation(summary = "获得车辆续保产品信息")
@PreAuthorize("@ss.hasPermission('car:renewal-product:query')")
public CommonResult<List<RenewalProductRespVO>> findByProductType() {
// 调用服务层的方法,查询所有符合条件的记录
List<RenewalProductDO> list = renewalProductService.findByProductType();
// 返回成功结果,转换为 VO 类型
return success(BeanUtils.toBean(list, RenewalProductRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出车辆续保产品信息 Excel")
@PreAuthorize("@ss.hasPermission('car:renewal-product:export')")

View File

@@ -6,14 +6,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.car.controller.admin.renewalproduct.vo.RenewalProductRespVO;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StorePageReqVO;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StoreRespVO;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StoreSaveReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalproduct.RenewalProductDO;
import cn.iocoder.yudao.module.car.dal.dataobject.store.StoreDO;
import cn.iocoder.yudao.module.car.service.store.StoreService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -71,6 +74,16 @@ public class StoreController {
return success(BeanUtils.toBean(pageResult, StoreRespVO.class));
}
@GetMapping("/findByProductType")
@Operation(summary = "获得车辆续保产品信息")
public CommonResult<List<StoreRespVO>> findByProductType() {
// 调用服务层的方法,查询所有符合条件的记录
List<StoreDO> list = storeService.findByProductType();
// 返回成功结果,转换为 VO 类型
return success(BeanUtils.toBean(list, StoreRespVO.class));
}
@GetMapping("/pageAll")
@Operation(summary = "获得门店管理分页-全部")
public CommonResult<PageResult<StoreRespVO>> getStorePageAll(@Valid StorePageReqVO pageReqVO) {

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.car.controller.admin.tireuser.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -17,7 +18,7 @@ public class TireUserPageReqVO extends PageParam {
private String queryType;
@Schema(description = "门店ID关联门店表", example = "27998")
private Long storeId;
private String storeId;
@Schema(description = "仓库ID关联仓库表", example = "27998")
private Long warehouseId;
@@ -25,6 +26,12 @@ public class TireUserPageReqVO extends PageParam {
@Schema(description = "用户ID关联用户表", example = "27998")
private Long userId;
@Schema(description = "产品类别car_renewal_product_type00 无忧01 延保)", example = "00")
private String productType;
@Schema(description = "用户表")
private UserPageReqVO user;
@Schema(description = "门店名称", example = "门店名称")
private String storeName;
}

View File

@@ -17,7 +17,7 @@ public class TireUserRespVO {
@Schema(description = "门店ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11176")
@ExcelProperty("门店ID")
private Long storeId;
private String storeId;
@Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11176")
@ExcelProperty("仓库编号")
@@ -28,7 +28,15 @@ public class TireUserRespVO {
@ExcelProperty("用户关联用户表")
private Long userId;
@Schema(description = "产品类别car_renewal_product_type00 无忧01 延保)", example = "00")
@ExcelProperty("产品类别car_renewal_product_type00 无忧01 延保)")
private String productType;
@Schema(description = "用户表")
@ExcelProperty("用户关联用户表")
private UserRespVO user;
@Schema(description = "门店名称", example = "门店名称")
@ExcelProperty("门店名称")
private String storeName;
}

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.car.controller.admin.tireuser.vo;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -12,7 +13,7 @@ public class TireUserSaveReqVO {
private Integer id;
@Schema(description = "门店编号", example = "我是一个用户")
private Long storeId;
private String storeId;
@Schema(description = "仓库编号", example = "我是一个用户")
private Long warehouseId;
@@ -20,6 +21,12 @@ public class TireUserSaveReqVO {
@Schema(description = "用户编号", example = "我是一个用户")
private Long userId;
@Schema(description = "产品类别car_renewal_product_type00 无忧01 延保)", example = "00")
private String productType;
@Schema(description = "用户表")
private UserSaveReqVO user;
@Schema(description = "门店名称", example = "门店名称")
private String storeName;
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.car.dal.dataobject.renewalorder;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.time.LocalDate;
@@ -135,6 +136,7 @@ public class RenewalOrderDO extends BaseDO {
* 合同备注
*/
private String contractRemark;
private String contractUrl;
private Long storeId;
@@ -144,4 +146,58 @@ public class RenewalOrderDO extends BaseDO {
private String invoiceUrl;
private String productType;
/**
* 客户签名
*/
private String customerSignatureUrl;
/**
* 身份证正面
*/
private String idCardFrontUrl;
/**
* 身份证反面
*/
private String idCardBackUrl;
/**
* 行驶证
*/
private String drivingLicenseUrl;
/**
* 购车发票多张JSON字符串
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> carInvoiceUrls;
/**
* 购置税发票多张JSON字符串
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> purchaseTaxInvoiceUrls;
/**
* 商业险保单多张JSON字符串
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> businessInsurancePolicyUrls;
/**
* 合格证
*/
private String certificateOfConformityUrl;
/**
* 里程表照片
*/
private String odometerPhotoUrl;
/**
* 车名牌照片
*/
private String nameplatePhotoUrl;
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
/**
@@ -32,7 +33,7 @@ public class TireUserDO extends BaseDO{
/**
* 门店ID
*/
private Long storeId;
private String storeId;
/**
* 仓库ID
@@ -44,6 +45,10 @@ public class TireUserDO extends BaseDO{
*/
private Long userId;
/**
* 产品类别car_renewal_product_type00 无忧01 延保)
*/
private String productType;
/**
* 用户信息(非数据库字段)
@@ -51,4 +56,12 @@ public class TireUserDO extends BaseDO{
@TableField(exist = false)
private AdminUserDO user;
/**
* 门店名称
*/
@TableField(exist = false)
private String storeName;
}

View File

@@ -45,8 +45,8 @@ public interface RenewalOrderMapper extends BaseMapperX<RenewalOrderDO> {
.eq(reqVO.getServiceBuyer() != null, RenewalOrderDO::getServiceBuyer, reqVO.getServiceBuyer())
.eq(reqVO.getCarBuyer() != null, RenewalOrderDO::getCarBuyer, reqVO.getCarBuyer())
.eq(reqVO.getCertType() != null, RenewalOrderDO::getCertType, reqVO.getCertType())
.eq(reqVO.getMobile() != null, RenewalOrderDO::getMobile, reqVO.getMobile())
.eq(reqVO.getCertNo() != null, RenewalOrderDO::getCertNo, reqVO.getCertNo())
.like(reqVO.getMobile() != null, RenewalOrderDO::getMobile, reqVO.getMobile())
.like(reqVO.getCertNo() != null, RenewalOrderDO::getCertNo, reqVO.getCertNo())
.eq(reqVO.getContactAddress() != null, RenewalOrderDO::getContactAddress, reqVO.getContactAddress())
.eq(reqVO.getMemberEmail() != null, RenewalOrderDO::getMemberEmail, reqVO.getMemberEmail())
.eq(reqVO.getProductId() != null, RenewalOrderDO::getProductId, reqVO.getProductId())
@@ -57,8 +57,10 @@ public interface RenewalOrderMapper extends BaseMapperX<RenewalOrderDO> {
.eq(reqVO.getProductFee() != null, RenewalOrderDO::getProductFee, reqVO.getProductFee())
.eq(reqVO.getSettlementMethod() != null, RenewalOrderDO::getSettlementMethod, reqVO.getSettlementMethod())
.eq(reqVO.getRemark() != null, RenewalOrderDO::getRemark, reqVO.getRemark())
.eq(reqVO.getInputUser() != null, RenewalOrderDO::getInputUser, reqVO.getInputUser())
.like(reqVO.getInputUser() != null, RenewalOrderDO::getInputUser, reqVO.getInputUser())
.eq(reqVO.getContractRemark() != null, RenewalOrderDO::getContractRemark, reqVO.getContractRemark())
.eq(reqVO.getStoreId() != null, RenewalOrderDO::getStoreId, reqVO.getStoreId())
.eq(reqVO.getCreator() != null, RenewalOrderDO::getCreator, reqVO.getCreator())
.orderByDesc(RenewalOrderDO::getId);
// 3. createTime 范围处理

View File

@@ -5,8 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.car.controller.admin.renewalproduct.vo.RenewalProductPageReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalproduct.RenewalProductDO;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 车辆续保产品信息 Mapper
*
@@ -25,4 +28,32 @@ public interface RenewalProductMapper extends BaseMapperX<RenewalProductDO> {
.orderByDesc(RenewalProductDO::getId));
}
default List<RenewalProductDO> findByProductType(String productType) {
if (StringUtils.isNotBlank(productType)) {
// 将 productType 按照逗号分割成数组
String[] types = productType.split(",");
StringBuilder sqlBuilder = new StringBuilder();
// 避免多余的 AND 拼接
for (int i = 0; i < types.length; i++) {
if (i > 0) {
sqlBuilder.append(" OR "); // 连接多个条件
}
// 使用 FIND_IN_SET 来检查每个 type 是否存在于 product_type 字段中
sqlBuilder.append("FIND_IN_SET('").append(types[i]).append("', product_type) > 0");
}
// 构造带有动态条件的查询
return selectList(new LambdaQueryWrapperX<RenewalProductDO>()
.apply("(" + sqlBuilder.toString() + ")") // 不要再加额外的 AND
.orderByDesc(RenewalProductDO::getId)); // 按照 id 排序,视情况调整
}
// 如果 productType 为空或 null查询所有数据
return selectList(new LambdaQueryWrapperX<RenewalProductDO>()
.orderByDesc(RenewalProductDO::getId)); // 按照 id 排序,返回所有数据
}
}

View File

@@ -6,6 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StorePageReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.store.StoreDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 门店管理 Mapper
@@ -23,7 +26,24 @@ public interface StoreMapper extends BaseMapperX<StoreDO> {
.eqIfPresent(StoreDO::getLatitude, reqVO.getLatitude())
.eqIfPresent(StoreDO::getLongitude, reqVO.getLongitude())
.eqIfPresent(StoreDO::getPostalCode, reqVO.getPostalCode())
.eqIfPresent(StoreDO::getPhoneNumber, reqVO.getPhoneNumber())
.likeIfPresent(StoreDO::getPhoneNumber, reqVO.getPhoneNumber())
.eqIfPresent(StoreDO::getEmail, reqVO.getEmail())
.likeIfPresent(StoreDO::getManagerName, reqVO.getManagerName())
.betweenIfPresent(StoreDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(StoreDO::getStatus, reqVO.getStatus())
.orderByDesc(StoreDO::getId));
}
default PageResult<StoreDO> selectPageByIds(StorePageReqVO reqVO, List<Long> storeIds) {
return selectPage(reqVO, new LambdaQueryWrapperX<StoreDO>()
.inIfPresent(StoreDO::getId, storeIds)
.likeIfPresent(StoreDO::getStoreName, reqVO.getStoreName())
.eqIfPresent(StoreDO::getAreaId, reqVO.getAreaId())
.eqIfPresent(StoreDO::getDetailedLocation, reqVO.getDetailedLocation())
.eqIfPresent(StoreDO::getLatitude, reqVO.getLatitude())
.eqIfPresent(StoreDO::getLongitude, reqVO.getLongitude())
.eqIfPresent(StoreDO::getPostalCode, reqVO.getPostalCode())
.likeIfPresent(StoreDO::getPhoneNumber, reqVO.getPhoneNumber())
.eqIfPresent(StoreDO::getEmail, reqVO.getEmail())
.likeIfPresent(StoreDO::getManagerName, reqVO.getManagerName())
.betweenIfPresent(StoreDO::getCreateTime, reqVO.getCreateTime())

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.car.service.contract;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalorder.RenewalOrderDO;
import java.io.OutputStream;
/**
* 合同生成服务接口
*
* @author 芋道源码
*/
public interface ContractService {
/**
* 生成合同 PDF
*
* @param renewalOrder 续保订单
* @param outputStream 输出流
* @return PDF 文件的 URL上传后的路径
*/
String generateContractPdf(RenewalOrderDO renewalOrder, OutputStream outputStream);
/**
* 生成合同 PDF 并上传
*
* @param renewalOrder 续保订单
* @return PDF 文件的 URL上传后的路径
*/
String generateAndUploadContract(RenewalOrderDO renewalOrder);
/**
* 生成合同 HTML用于预览
*
* @param renewalOrder 续保订单
* @return 渲染后的 HTML 字符串
*/
String generateContractHtml(RenewalOrderDO renewalOrder);
/**
* 计算合同 SHA256 Hash用于防篡改
*
* @param pdfBytes PDF 文件字节数组
* @return SHA256 Hash 值
*/
String calculateContractHash(byte[] pdfBytes);
}

View File

@@ -0,0 +1,551 @@
package cn.iocoder.yudao.module.car.service.contract;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalorder.RenewalOrderDO;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import cn.hutool.core.util.StrUtil;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.regex.Pattern;
/**
* 合同生成服务实现类
*
* @author 芋道源码
*/
@Service
@Slf4j
public class ContractServiceImpl implements ContractService {
@Resource
private TemplateEngine templateEngine;
@Resource
private FileApi fileApi;
@Resource
private FileService fileService;
private static final String CONTRACT_TEMPLATE = "contract/renewal-contract";
private static final String SEAL_IMAGE_PATH = "static/seal.png";
private static final String CONTRACT_PDF_DIR = "contractpdf";
private static final Pattern FILE_NAME_ILLEGAL_CHARS = Pattern.compile("[\\\\/:*?\"<>|\\n\\r\\t]");
// 缓存字体文件路径,避免重复查找
private static volatile String cachedFontPath = null;
// 中文字体路径(使用系统字体或资源文件)
private static final String[] FONT_PATHS = {
"/System/Library/Fonts/Hiragino Sans GB.ttc", // macOS 冬青黑体简体中文(推荐)
"/System/Library/Fonts/STHeiti Light.ttc", // macOS 黑体-细体
"/System/Library/Fonts/STHeiti Medium.ttc", // macOS 黑体-中等
"/System/Library/Fonts/PingFang.ttc", // macOS 苹方(如果存在)
"C:/Windows/Fonts/simsun.ttc", // Windows 宋体
"C:/Windows/Fonts/simhei.ttf", // Windows 黑体
"C:/Windows/Fonts/msyh.ttc", // Windows 微软雅黑
"C:/Windows/Fonts/msyhbd.ttc", // Windows 微软雅黑 Bold
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", // Linux 文泉驿微米黑
"/usr/share/fonts/truetype/arphic/uming.ttc", // Linux 文鼎字体
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf" // Linux Droid
};
@Override
public String generateContractPdf(RenewalOrderDO renewalOrder, OutputStream outputStream) {
try {
// 1. 准备模板数据
Context context = prepareTemplateContext(renewalOrder);
// 2. 渲染 HTML 模板
String html = templateEngine.process(CONTRACT_TEMPLATE, context);
log.debug("HTML 模板渲染成功,长度: {}", html.length());
// 3. 将 HTML 转换为 PDF
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.withHtmlContent(html, null);
builder.toStream(outputStream);
// 配置中文字体支持(必须在 withHtmlContent 之后run() 之前)
configureChineseFonts(builder);
log.info("开始生成 PDF...");
builder.run();
log.info("PDF 生成成功");
return null; // 直接输出到流,不返回 URL
} catch (Exception e) {
log.error("生成合同 PDF 失败", e);
// 不输出完整的堆栈信息中的敏感内容,只输出错误消息
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.length() > 500) {
errorMsg = errorMsg.substring(0, 500) + "...";
}
throw new RuntimeException("生成合同 PDF 失败: " + errorMsg, e);
}
}
/**
* 生成合同 HTML用于预览
*/
@Override
public String generateContractHtml(RenewalOrderDO renewalOrder) {
Context context = prepareTemplateContext(renewalOrder);
return templateEngine.process(CONTRACT_TEMPLATE, context);
}
@Override
public String generateAndUploadContract(RenewalOrderDO renewalOrder) {
try {
// 1. 生成 PDF 到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
generateContractPdf(renewalOrder, baos);
byte[] pdfBytes = baos.toByteArray();
// 2. 计算 SHA256 Hash用于防篡改
String hash = calculateContractHash(pdfBytes);
log.info("合同 PDF Hash: {}", hash);
// 3. 上传文件
String displayName = generateDisplayName(renewalOrder);
String storagePath = generateStoragePath(renewalOrder);
// FileApi 签名createFile(name, path, content)
String url = fileApi.createFile(displayName, storagePath, pdfBytes);
return url;
} catch (Exception e) {
log.error("生成并上传合同失败", e);
throw new RuntimeException("生成并上传合同失败: " + e.getMessage(), e);
}
}
@Override
public String calculateContractHash(byte[] pdfBytes) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(pdfBytes);
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
log.error("计算合同 Hash 失败", e);
throw new RuntimeException("计算合同 Hash 失败: " + e.getMessage(), e);
}
}
/**
* 准备模板上下文数据
*/
private Context prepareTemplateContext(RenewalOrderDO renewalOrder) {
Context context = new Context();
// 基础信息
context.setVariable("order", renewalOrder);
// 凭证编号TABLA + 日期 + 序号使用订单ID
String certificateNo = generateCertificateNo(renewalOrder);
context.setVariable("certificateNo", certificateNo);
// 服务期限
LocalDate startDate = LocalDate.now();
LocalDate endDate = startDate.plusYears(3);
context.setVariable("serviceStartDate", startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
context.setVariable("serviceEndDate", endDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
// 服务期限(用于权益说明)
context.setVariable("serviceStartDateText", startDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
context.setVariable("serviceEndDateText", endDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
// 签章图片路径(转换为 base64
// 注意openhtmltopdf 可能不支持 WebP 格式,如果出错会跳过签章显示
try {
String sealBase64 = loadSealImageAsBase64();
context.setVariable("sealImageBase64", sealBase64 != null ? sealBase64 : "");
if (sealBase64 == null || sealBase64.isEmpty()) {
log.warn("签章图片未加载PDF 中将不显示签章");
}
} catch (Exception e) {
log.warn("加载签章图片时出错,将不显示签章: {}", e.getMessage());
context.setVariable("sealImageBase64", "");
}
// 客户签名图片(转换为 base64
// 如果订单中有客户签名URL需要转换为base64用于PDF生成
log.info("========== 开始处理客户签名 ==========");
log.info("订单ID: {}", renewalOrder.getId());
log.info("客户签名URL: {}", renewalOrder.getCustomerSignatureUrl());
String customerSignatureBase64 = "";
if (renewalOrder.getCustomerSignatureUrl() != null && !renewalOrder.getCustomerSignatureUrl().isEmpty()) {
try {
log.info("开始加载客户签名图片: {}", renewalOrder.getCustomerSignatureUrl());
customerSignatureBase64 = loadCustomerSignatureAsBase64(renewalOrder.getCustomerSignatureUrl());
if (customerSignatureBase64 == null || customerSignatureBase64.isEmpty()) {
log.warn("客户签名图片未加载PDF 中将不显示客户签名");
} else {
log.info("客户签名图片加载成功base64长度: {}", customerSignatureBase64.length());
log.info("base64前缀: {}", customerSignatureBase64.length() > 50 ? customerSignatureBase64.substring(0, 50) + "..." : customerSignatureBase64);
}
} catch (Exception e) {
log.error("加载客户签名图片时出错,将不显示客户签名: {}", e.getMessage(), e);
}
} else {
log.warn("========== 订单中没有客户签名URLPDF中将不显示客户签名 ==========");
}
context.setVariable("customerSignatureBase64", customerSignatureBase64);
log.info("设置模板变量 customerSignatureBase64: {}", customerSignatureBase64 != null && !customerSignatureBase64.isEmpty() ? "已设置(长度: " + customerSignatureBase64.length() + "" : "为空");
log.info("========== 客户签名处理完成 ==========");
// 防篡改 Hash模板展示用最终 Hash 以生成后 PDF 计算为准;此处先放占位/可不展示)
context.setVariable("hash", "");
return context;
}
/**
* 生成凭证编号
*/
private String generateCertificateNo(RenewalOrderDO renewalOrder) {
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String idStr = String.format("%03d", renewalOrder.getId() % 1000);
return "TABLA" + dateStr + idStr;
}
/**
* 生成文件名
*/
private String generateStoragePath(RenewalOrderDO renewalOrder) {
// MinIO 存储路径必须增加 /contractpdf 前缀
// path 不要以 / 开头,避免不同存储适配器对绝对路径处理不一致
return CONTRACT_PDF_DIR + "/" + generateDisplayName(renewalOrder);
}
/** 生成展示名称(用于文件记录展示) */
private String generateDisplayName(RenewalOrderDO renewalOrder) {
// 客户名称:优先使用车主/客户名称carBuyer为空则兜底“客户”
String customerName = renewalOrder.getCarBuyer();
if (customerName == null || customerName.trim().isEmpty()) {
customerName = "客户";
}
customerName = sanitizeFileName(customerName.trim());
// 时间戳:毫秒
long ts = System.currentTimeMillis();
return customerName + "-途安出行保障服务凭证(" + ts + ").pdf";
}
private String sanitizeFileName(String name) {
// 替换非法字符,避免 MinIO/浏览器下载时文件名异常
String s = FILE_NAME_ILLEGAL_CHARS.matcher(name).replaceAll("_");
// 控制长度(避免过长导致部分浏览器/存储不兼容)
if (s.length() > 60) {
s = s.substring(0, 60);
}
return s;
}
/**
* 配置中文字体支持
*/
private void configureChineseFonts(PdfRendererBuilder builder) {
try {
// 查找可用的中文字体文件
log.info("开始查找中文字体文件...");
InputStream fontStream = findChineseFontStream();
if (fontStream == null) {
log.error("❌ 未找到中文字体文件!");
log.error("请确认:");
log.error("1. 字体文件已放到 src/main/resources/fonts/ 目录下");
log.error("2. 项目已重新编译mvn clean compile");
log.error("3. 字体文件已打包到 jar 中");
log.error("支持的字体文件名simsun.ttc, simsun.ttf, simhei.ttf, msyh.ttc, PingFang.ttc");
// 列出所有尝试的路径用于诊断
log.error("诊断信息 - 尝试过的资源路径:");
String[] testPaths = {"fonts/chinese.ttf", "fonts/simsun.ttc", "fonts/simsun.ttf", "fonts/Hiragino Sans GB.ttc"};
for (String path : testPaths) {
try {
ClassPathResource testRes = new ClassPathResource(path);
boolean exists = testRes.exists();
log.error(" {} -> {}", path, exists ? "✓ 找到" : "✗ 未找到");
} catch (Exception e) {
log.error(" {} -> 检查失败: {}", path, e.getMessage());
}
}
throw new RuntimeException("未找到中文字体文件,无法生成包含中文的 PDF");
}
// 测试读取字体文件
try {
byte[] testBytes = new byte[10];
fontStream.read(testBytes);
fontStream.close();
log.info("字体文件验证成功,文件大小: {} bytes", testBytes.length);
} catch (Exception e) {
log.error("字体文件读取失败", e);
throw new RuntimeException("字体文件读取失败: " + e.getMessage());
}
// useFont 方法需要 Supplier<InputStream>,每次调用都会重新打开流
// 这里需要确保每次返回的流都是有效的
// 关键只注册一个字体族SimSun避免 openhtmltopdf 重复尝试加载不存在的字体族导致 fallback 为 null
builder.useFont(() -> {
try {
InputStream stream = findChineseFontStream();
if (stream == null) {
log.error("字体流为 null");
}
return stream;
} catch (Exception e) {
log.error("加载字体流失败", e);
return null;
}
}, "SimSun");
log.info("成功配置中文字体");
} catch (Exception e) {
log.error("配置中文字体失败", e);
throw new RuntimeException("配置中文字体失败: " + e.getMessage(), e);
}
}
/**
* 查找中文字体文件并返回 InputStream
*/
private InputStream findChineseFontStream() {
// 如果已缓存字体路径,直接使用
if (cachedFontPath != null) {
try {
java.io.File fontFile = new java.io.File(cachedFontPath);
if (fontFile.exists() && fontFile.canRead()) {
return new java.io.FileInputStream(fontFile);
} else {
cachedFontPath = null; // 缓存失效,重新查找
}
} catch (Exception e) {
log.warn("使用缓存的字体路径失败,重新查找: {}", cachedFontPath, e);
cachedFontPath = null;
}
}
// 方法1尝试从资源文件加载字体
String[] resourceFontNames = {
// 优先:从 TTC 提取出来的单字体(避免 openhtmltopdf/pdfbox 对 .ttc 兼容性问题)
"fonts/chinese.ttf",
// 兼容:旧的字体路径
"fonts/simsun.ttf",
"fonts/simhei.ttf",
"fonts/msyh.ttc",
"fonts/PingFang.ttc",
"fonts/simsun.ttc",
"fonts/Hiragino Sans GB.ttc"
};
for (String fontName : resourceFontNames) {
try {
ClassPathResource fontResource = new ClassPathResource(fontName);
if (fontResource.exists()) {
try {
// 尝试获取文件的绝对路径
java.io.File file = fontResource.getFile();
if (file != null && file.exists()) {
cachedFontPath = file.getAbsolutePath();
log.info("✓ 从资源文件找到字体(绝对路径): {}", cachedFontPath);
return new java.io.FileInputStream(file);
}
} catch (Exception e) {
// 如果不是文件系统资源(可能是 jar 包内),使用流
try {
InputStream stream = fontResource.getInputStream();
if (stream != null) {
log.info("✓ 从资源文件加载字体流: {}", fontName);
// 对于 jar 内的资源,无法缓存路径,直接返回流
return stream;
}
} catch (Exception e2) {
log.debug("读取字体资源文件失败: {}", fontName, e2);
}
}
}
} catch (Exception e) {
log.debug("检查资源字体文件失败: {}", fontName, e);
}
}
// 方法2尝试加载系统字体
log.info("资源文件中未找到字体,尝试从系统路径加载...");
for (String fontPath : FONT_PATHS) {
try {
java.io.File fontFile = new java.io.File(fontPath);
if (fontFile.exists() && fontFile.isFile() && fontFile.canRead()) {
cachedFontPath = fontPath;
log.info("✓ 从系统路径加载字体成功: {}", fontPath);
return new java.io.FileInputStream(fontFile);
}
} catch (Exception e) {
log.debug("加载系统字体失败: {}", fontPath, e);
}
}
log.error("❌ 未找到任何中文字体文件!");
log.error("请确认1. 字体文件在 src/main/resources/fonts/ 目录下");
log.error(" 2. 项目已重新编译");
log.error(" 3. 字体文件已打包到 jar 中");
return null;
}
/**
* 加载签章图片并转换为 Base64PNG
*/
private String loadSealImageAsBase64() {
try {
ClassPathResource resource = new ClassPathResource(SEAL_IMAGE_PATH);
if (!resource.exists()) {
log.warn("签章图片文件不存在: {}", SEAL_IMAGE_PATH);
return "";
}
try (InputStream inputStream = resource.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
byte[] data = new byte[8192];
int nRead;
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
byte[] imageBytes = buffer.toByteArray();
if (imageBytes.length == 0) {
log.warn("签章图片文件为空: {}", SEAL_IMAGE_PATH);
return "";
}
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
return "data:image/png;base64," + base64;
}
} catch (Exception e) {
log.warn("加载签章图片失败,将不显示签章: {}", e.getMessage());
return "";
}
}
/**
* 检测图片类型
*/
private String detectImageType(byte[] imageBytes) {
if (imageBytes.length < 4) {
return "image/png"; // 默认
}
// 检查文件头
// PNG: 89 50 4E 47
if (imageBytes[0] == (byte)0x89 && imageBytes[1] == 0x50 &&
imageBytes[2] == 0x4E && imageBytes[3] == 0x47) {
return "image/png";
}
// JPEG: FF D8 FF
if (imageBytes[0] == (byte)0xFF && imageBytes[1] == (byte)0xD8 && imageBytes[2] == (byte)0xFF) {
return "image/jpeg";
}
// GIF: 47 49 46 38
if (imageBytes[0] == 0x47 && imageBytes[1] == 0x49 &&
imageBytes[2] == 0x46 && imageBytes[3] == 0x38) {
return "image/gif";
}
// WebP: RIFF...WEBP
if (imageBytes.length >= 12 &&
imageBytes[0] == 0x52 && imageBytes[1] == 0x49 &&
imageBytes[2] == 0x46 && imageBytes[3] == 0x46 &&
imageBytes[8] == 0x57 && imageBytes[9] == 0x45 &&
imageBytes[10] == 0x42 && imageBytes[11] == 0x50) {
return "image/webp";
}
// 默认返回 PNG
return "image/png";
}
/**
* 加载客户签名图片并转换为 Base64
*
* @param signatureUrl 客户签名图片的 URL
* @return Base64 编码的图片字符串data:image/png;base64,... 格式)
*/
private String loadCustomerSignatureAsBase64(String signatureUrl) {
if (signatureUrl == null || signatureUrl.isEmpty()) {
return "";
}
try {
byte[] imageBytes = null;
// 方法1尝试从 URL 中提取 configId 和 path使用 FileService 获取
// URL 格式:{domain}/admin-api/infra/file/{configId}/get/{path}
if (signatureUrl.contains("/admin-api/infra/file/") && signatureUrl.contains("/get/")) {
try {
// 提取 configId 和 path
String[] parts = signatureUrl.split("/admin-api/infra/file/");
if (parts.length == 2) {
String afterPrefix = parts[1];
String[] configAndPath = afterPrefix.split("/get/", 2);
if (configAndPath.length == 2) {
Long configId = Long.parseLong(configAndPath[0]);
String path = configAndPath[1];
// URL 解码路径
path = java.net.URLDecoder.decode(path, "UTF-8");
// 使用 FileService 获取文件内容
imageBytes = fileService.getFileContent(configId, path);
log.info("通过 FileService 成功加载客户签名: configId={}, path={}", configId, path);
}
}
} catch (Exception e) {
log.debug("从 URL 提取路径失败,尝试直接下载: {}", e.getMessage());
}
}
// 方法2如果方法1失败直接通过 HTTP 下载
if (imageBytes == null || imageBytes.length == 0) {
try {
java.net.URL url = new java.net.URL(signatureUrl);
try (InputStream inputStream = url.openStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
byte[] data = new byte[8192];
int nRead;
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
imageBytes = buffer.toByteArray();
log.info("通过 HTTP 直接下载成功加载客户签名: {}", signatureUrl);
}
} catch (Exception e) {
log.warn("通过 HTTP 下载客户签名失败: {}", e.getMessage());
}
}
if (imageBytes == null || imageBytes.length == 0) {
log.warn("客户签名图片文件为空: {}", signatureUrl);
return "";
}
// 检测图片类型
String imageType = detectImageType(imageBytes);
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
return "data:" + imageType + ";base64," + base64;
} catch (Exception e) {
log.error("加载客户签名图片失败,将不显示客户签名: {} - {}", signatureUrl, e.getMessage(), e);
return "";
}
}
}

View File

@@ -52,4 +52,20 @@ public interface RenewalOrderService {
*/
PageResult<RenewalOrderDO> getRenewalOrderPage(RenewalOrderPageReqVO pageReqVO);
/**
* 生成合同 PDF在线并上传回写订单的 contractUrl
*
* @param id 订单编号
* @return 合同 URL
*/
String generateContract(Long id);
/**
* 生成合同 HTML用于预览
*
* @param id 订单编号
* @return 合同 HTML 字符串
*/
String generateContractHtml(Long id);
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.car.service.renewalorder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
@@ -13,6 +14,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.car.dal.mysql.renewalorder.RenewalOrderMapper;
import cn.iocoder.yudao.module.car.service.contract.ContractService;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.*;
@@ -22,6 +24,7 @@ import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.*;
*
* @author 芋道源码
*/
@Slf4j
@Service
@Validated
public class RenewalOrderServiceImpl implements RenewalOrderService {
@@ -29,10 +32,17 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
@Resource
private RenewalOrderMapper renewalOrderMapper;
@Resource
private ContractService contractService;
@Override
public Long createRenewalOrder(RenewalOrderSaveReqVO createReqVO) {
// 插入
RenewalOrderDO renewalOrder = BeanUtils.toBean(createReqVO, RenewalOrderDO.class);
// 数据库字段 contract_url 为 NOT NULL 且无默认值,新增时必须给一个兜底值
if (renewalOrder.getContractUrl() == null) {
renewalOrder.setContractUrl("");
}
renewalOrderMapper.insert(renewalOrder);
// 返回
return renewalOrder.getId();
@@ -71,4 +81,30 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
return renewalOrderMapper.selectPage(pageReqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String generateContract(Long id) {
RenewalOrderDO order = renewalOrderMapper.selectById(id);
if (order == null) {
throw exception(RENEWAL_ORDER_NOT_EXISTS);
}
// 添加日志,确认订单数据是否正确加载
log.info("生成合同 - 订单ID: {}, 客户签名URL: {}", id, order.getCustomerSignatureUrl());
String url = contractService.generateAndUploadContract(order);
// 回写合同 URL
RenewalOrderDO update = new RenewalOrderDO();
update.setId(id);
update.setContractUrl(url);
renewalOrderMapper.updateById(update);
return url;
}
@Override
public String generateContractHtml(Long id) {
RenewalOrderDO order = renewalOrderMapper.selectById(id);
if (order == null) {
throw exception(RENEWAL_ORDER_NOT_EXISTS);
}
return contractService.generateContractHtml(order);
}
}

View File

@@ -52,4 +52,5 @@ public interface RenewalProductService {
*/
PageResult<RenewalProductDO> getRenewalProductPage(RenewalProductPageReqVO pageReqVO);
List<RenewalProductDO> findByProductType();
}

View File

@@ -1,11 +1,19 @@
package cn.iocoder.yudao.module.car.service.renewalproduct;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.car.dal.dataobject.tireuser.TireUserDO;
import cn.iocoder.yudao.module.car.service.tireuser.TireUserService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import cn.iocoder.yudao.module.car.controller.admin.renewalproduct.vo.*;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalproduct.RenewalProductDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -28,30 +36,30 @@ public class RenewalProductServiceImpl implements RenewalProductService {
@Resource
private RenewalProductMapper renewalProductMapper;
@Resource
private TireUserService tireUserService;
@Resource
private PermissionService permissionService;
// ================== CRUD ==================
@Override
public Long createRenewalProduct(RenewalProductSaveReqVO createReqVO) {
// 插入
RenewalProductDO renewalProduct = BeanUtils.toBean(createReqVO, RenewalProductDO.class);
renewalProductMapper.insert(renewalProduct);
// 返回
return renewalProduct.getId();
}
@Override
public void updateRenewalProduct(RenewalProductSaveReqVO updateReqVO) {
// 校验存在
validateRenewalProductExists(updateReqVO.getId());
// 更新
RenewalProductDO updateObj = BeanUtils.toBean(updateReqVO, RenewalProductDO.class);
renewalProductMapper.updateById(updateObj);
}
@Override
public void deleteRenewalProduct(Long id) {
// 校验存在
validateRenewalProductExists(id);
// 删除
renewalProductMapper.deleteById(id);
}
@@ -71,4 +79,23 @@ public class RenewalProductServiceImpl implements RenewalProductService {
return renewalProductMapper.selectPage(pageReqVO);
}
}
// ================== 业务查询 ==================
@Override
public List<RenewalProductDO> findByProductType() {
Long userId = SecurityFrameworkUtils.getLoginUserId();
// 租户管理员:查全部
if (permissionService.hasAnyRoles(userId, "tenant_admin")) {
return renewalProductMapper.findByProductType(null);
}
// 普通用户:按用户配置的产品类型查
TireUserDO tireUsers = tireUserService.findByUserId(userId);
if (StringUtils.isBlank(tireUsers.getProductType())) {
return Collections.emptyList();
}
return renewalProductMapper.findByProductType(tireUsers.getProductType());
}
}

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.car.controller.admin.store.vo.StoreSaveReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.store.StoreDO;
import javax.validation.Valid;
import java.util.List;
/**
* 门店管理 Service 接口
@@ -52,4 +53,5 @@ public interface StoreService {
*/
PageResult<StoreDO> getStorePage(StorePageReqVO pageReqVO);
List<StoreDO> findByProductType();
}

View File

@@ -2,14 +2,21 @@ package cn.iocoder.yudao.module.car.service.store;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StorePageReqVO;
import cn.iocoder.yudao.module.car.controller.admin.store.vo.StoreSaveReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.store.StoreDO;
import cn.iocoder.yudao.module.car.dal.mysql.store.StoreMapper;
import cn.iocoder.yudao.module.car.service.tireuser.TireUserService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.STORE_NOT_EXISTS;
@@ -25,47 +32,97 @@ public class StoreServiceImpl implements StoreService {
@Resource
private StoreMapper storeMapper;
@Resource
private PermissionService permissionService;
@Resource
private TireUserService tireUserService;
// ==================== CRUD ====================
@Override
public Integer createStore(StoreSaveReqVO createReqVO) {
// 插入
StoreDO store = BeanUtils.toBean(createReqVO, StoreDO.class);
storeMapper.insert(store);
// 返回
return store.getId();
}
@Override
public void updateStore(StoreSaveReqVO updateReqVO) {
// 校验存在
validateStoreExists(updateReqVO.getId());
// 更新
StoreDO updateObj = BeanUtils.toBean(updateReqVO, StoreDO.class);
storeMapper.updateById(updateObj);
}
@Override
public void deleteStore(Integer id) {
// 校验存在
validateStoreExists(id);
// 删除
storeMapper.deleteById(id);
}
@Override
public StoreDO getStore(Integer id) {
return storeMapper.selectById(id);
}
private void validateStoreExists(Integer id) {
if (storeMapper.selectById(id) == null) {
throw exception(STORE_NOT_EXISTS);
}
}
@Override
public StoreDO getStore(Integer id) {
return storeMapper.selectById(id);
}
// ==================== Query ====================
@Override
public PageResult<StoreDO> getStorePage(StorePageReqVO pageReqVO) {
return storeMapper.selectPage(pageReqVO);
if (isTenantAdmin()) {
return storeMapper.selectPage(pageReqVO);
}
List<Long> storeIds = getCurrentUserStoreIds();
if (storeIds.isEmpty()) {
return PageResult.empty();
}
return storeMapper.selectPageByIds(pageReqVO, storeIds);
}
}
/**
* 当前用户可访问的门店列表(用于下拉框等场景)
*/
@Override
public List<StoreDO> findByProductType() {
if (isTenantAdmin()) {
return storeMapper.selectList();
}
List<Long> storeIds = getCurrentUserStoreIds();
if (storeIds.isEmpty()) {
return Collections.emptyList();
}
return storeMapper.selectBatchIds(storeIds);
}
// ==================== Private ====================
private boolean isTenantAdmin() {
Long userId = SecurityFrameworkUtils.getLoginUserId();
return permissionService.hasAnyRoles(userId, "tenant_admin");
}
private List<Long> getCurrentUserStoreIds() {
Long userId = SecurityFrameworkUtils.getLoginUserId();
String storeIds = tireUserService.findByUserId(userId).getStoreId();
if (storeIds == null || storeIds.trim().isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(storeIds.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::valueOf)
.collect(Collectors.toList());
}
}

View File

@@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.car.controller.admin.tireuser.vo.TireUserPageReqV
import cn.iocoder.yudao.module.car.controller.admin.tireuser.vo.TireUserSaveReqVO;
import cn.iocoder.yudao.module.car.dal.dataobject.tireuser.TireUserDO;
import java.util.List;
/**
* 门店用户管理 Service 接口
*

View File

@@ -45,12 +45,7 @@ public class TireUserServiceImpl implements TireUserService {
@Transactional
public Integer createTireUser(TireUserSaveReqVO createReqVO) {
Long userId = adminUserService.createUser(createReqVO.getUser());
Long storeId = createReqVO.getStoreId();
Long warehouseId = createReqVO.getWarehouseId();
TireUserDO entity = new TireUserDO();
entity.setStoreId(storeId);
entity.setWarehouseId(warehouseId);
TireUserDO entity = BeanUtils.toBean(createReqVO, TireUserDO.class);
entity.setUserId(userId);
return tireUserMapper.insert(entity);
}

View File

@@ -0,0 +1,2 @@
# 此文件用于确保 fonts 目录被 Git 追踪
# 字体文件 simsun.ttc 应该放在此目录下

View File

@@ -0,0 +1,51 @@
# 中文字体文件说明
## 字体文件要求
为了正确显示中文,需要在此目录下放置中文字体文件。
## 推荐字体
1. **SimSun (宋体)** - `simsun.ttc``simsun.ttf`
- Windows 系统自带,路径:`C:/Windows/Fonts/simsun.ttc`
- 适合正式文档
2. **SimHei (黑体)** - `simhei.ttf`
- Windows 系统自带,路径:`C:/Windows/Fonts/simhei.ttf`
- 适合标题
3. **Microsoft YaHei (微软雅黑)** - `msyh.ttc`
- Windows 系统自带,路径:`C:/Windows/Fonts/msyh.ttc`
- 现代美观
4. **PingFang SC (苹方)** - `PingFang.ttc`
- macOS 系统自带,路径:`/System/Library/Fonts/PingFang.ttc`
## 如何获取字体文件
### Windows 系统
1. 打开 `C:/Windows/Fonts/` 目录
2. 找到 `simsun.ttc`(宋体)或 `msyh.ttc`(微软雅黑)
3. 复制到本目录,重命名为 `simsun.ttc`
### macOS 系统
1. 打开 `/System/Library/Fonts/` 目录
2. 找到 `PingFang.ttc`(苹方)
3. 复制到本目录,重命名为 `simsun.ttc`
### Linux 系统
```bash
# 安装文泉驿字体
sudo apt-get install fonts-wqy-microhei
# 或
sudo yum install wqy-microhei-fonts
# 然后从系统字体目录复制
cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ./simsun.ttc
```
## 注意事项
- 字体文件有版权限制,请确保您有使用权限
- 建议使用系统自带的免费字体
- 文件命名:建议使用 `simsun.ttc``simsun.ttf`(代码中优先查找此文件名)

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# 检查字体文件的脚本
echo "=========================================="
echo "检查字体文件"
echo "=========================================="
echo ""
FONT_DIR="./"
FONT_FILES=("simsun.ttc" "simsun.ttf" "simhei.ttf" "msyh.ttc" "PingFang.ttc")
found=false
for font in "${FONT_FILES[@]}"; do
if [ -f "$FONT_DIR/$font" ]; then
size=$(ls -lh "$FONT_DIR/$font" | awk '{print $5}')
echo "✓ 找到字体文件: $font (大小: $size)"
found=true
else
echo "✗ 未找到: $font"
fi
done
echo ""
if [ "$found" = false ]; then
echo "=========================================="
echo "❌ 错误:未找到任何字体文件!"
echo "=========================================="
echo ""
echo "请按照以下步骤操作:"
echo ""
echo "【Windows 系统】"
echo "1. 打开 C:\\Windows\\Fonts 目录"
echo "2. 复制 simsun.ttc 文件"
echo "3. 粘贴到本目录fonts 目录)"
echo ""
echo "【macOS 系统】"
echo "执行命令:"
echo " cp /System/Library/Fonts/PingFang.ttc ./simsun.ttc"
echo ""
echo "【Linux 系统】"
echo "执行命令:"
echo " sudo cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ./simsun.ttc"
echo ""
else
echo "=========================================="
echo "✓ 字体文件已就绪"
echo "=========================================="
fi

View File

@@ -0,0 +1,35 @@
==========================================
如何获取中文字体文件
==========================================
由于字体文件有版权限制,我无法直接下载。请按以下步骤操作:
【Windows 系统】
1. 打开文件资源管理器
2. 进入 C:\Windows\Fonts 目录
3. 找到以下字体文件之一:
- simsun.ttc (宋体) - 推荐
- msyh.ttc (微软雅黑)
- simhei.ttf (黑体)
4. 复制该字体文件
5. 粘贴到本目录fonts 目录)
6. 重命名为 simsun.ttc如果原文件名不同
【macOS 系统】
1. 打开 Finder
2. 按 Command+Shift+G 打开"前往文件夹"
3. 输入:/System/Library/Fonts
4. 找到 PingFang.ttc 或 STSong.ttc
5. 复制到本目录,重命名为 simsun.ttc
【Linux 系统】
在终端执行:
sudo cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ./simsun.ttc
或者安装字体后复制:
sudo apt-get install fonts-wqy-microhei
sudo cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ./simsun.ttc
==========================================
放置字体文件后,重新编译项目即可生效
==========================================

View File

@@ -8,5 +8,4 @@
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>

View File

@@ -3,25 +3,41 @@
<mapper namespace="cn.iocoder.yudao.module.car.dal.mysql.tireuser.TireUserMapper">
<select id="selectPage" resultType="cn.iocoder.yudao.module.car.dal.dataobject.tireuser.TireUserDO">
SELECT t1.*
SELECT t1.*,t3.store_name as storeName
FROM tire_user t1
LEFT JOIN system_users t2 on t2.id = t1.user_id
LEFT JOIN tire_store t3 on t3.id = t1.store_id
WHERE t1.deleted = false and t2.id != '0'
<if test="reqVO.userId != null">
AND t1.user_id = #{reqVO.userId}
</if>
<if test="reqVO.storeId != null">
AND t1.store_id = #{reqVO.storeId}
<if test="reqVO.storeId != null and reqVO.storeId != ''">
AND (
<!-- 遍历每个查询类型,检查是否存在于 product_type 字段中 -->
<foreach collection="reqVO.storeId.split(',')" item="type" separator=" OR " open="(" close=")">
FIND_IN_SET(#{type}, t1.store_id) > 0
</foreach>
)
</if>
<if test="reqVO.warehouseId != null">
AND t1.warehouse_id = #{reqVO.warehouseId}
</if>
<if test="reqVO.productType != null and reqVO.productType != ''">
AND (
<!-- 遍历每个查询类型,检查是否存在于 product_type 字段中 -->
<foreach collection="reqVO.productType.split(',')" item="type" separator=" OR " open="(" close=")">
FIND_IN_SET(#{type}, t1.product_type) > 0
</foreach>
)
</if>
<if test="reqVO.user != null">
<if test="reqVO.user.username != null and !reqVO.user.username.isEmpty()">
AND t2.username = #{reqVO.user.username}
<!-- 修改为模糊查询 -->
<if test="reqVO.user.username != null and reqVO.user.username != ''">
AND t2.username LIKE CONCAT('%', #{reqVO.user.username}, '%')
</if>
<if test="reqVO.user.mobile != null and !reqVO.user.mobile.isEmpty()">
AND t2.mobile = #{reqVO.user.mobile}
<!-- 修改为模糊查询 -->
<if test="reqVO.user.mobile != null and reqVO.user.mobile != ''">
AND t2.mobile LIKE CONCAT('%', #{reqVO.user.mobile}, '%')
</if>
<if test="reqVO.user.status != null">
AND t2.status = #{reqVO.user.status}
@@ -35,6 +51,7 @@
</if>
ORDER BY id DESC
</select>
<select id="findByUserId" resultType="cn.iocoder.yudao.module.car.dal.dataobject.tireuser.TireUserDO">
SELECT t1.*
FROM tire_user t1
@@ -45,4 +62,4 @@
</if>
ORDER BY id DESC
</select>
</mapper>
</mapper>

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

View File

@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>途安出行+保障服务凭证</title>
<style>
/* ======================
* 合同标准排版OpenHTMLToPDF
* ====================== */
@page {
size: A4;
/* 目标 2 页:压缩页边距 */
margin: 12mm 12mm 12mm 12mm;
@bottom-right {
content: counter(page) "页(总" counter(pages) "页)";
font-family: "SimSun", serif;
font-size: 8.5pt;
color: #666;
}
}
@page :first { margin-top: 8mm; }
html, body {
font-family: "SimSun", serif !important;
font-size: 10.5pt;
color: #111;
}
body {
margin: 0;
padding: 0;
line-height: 1.42;
}
.content { margin: 0; padding: 0; }
/* 水印:每页 1 个居中显示OpenHTMLToPDF 下不要用 z-index:-1可能被压到纸张底色下导致不可见 */
.watermark-layer {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 底层但可见 */
z-index: 0;
pointer-events: none;
white-space: nowrap;
}
.watermark-layer .wm {
font-size: 68pt;
font-weight: 700;
color: #eaeaea;
letter-spacing: 4pt;
}
.content {
position: relative;
z-index: 1;
}
h1 {
text-align: center;
font-size: 16pt;
font-weight: 700;
margin: 0 0 2px;
letter-spacing: 0.5px;
}
.meta { margin: 0 0 2px; }
.row { margin: 1px 0; }
.label { font-weight: 700; }
.section { margin-top: 4px; }
.title {
font-weight: 700;
margin: 4px 0 2px;
font-size: 11pt;
}
p {
margin: 0 0 2px;
text-indent: 2em;
text-align: justify;
}
.section > div:not(.title):not(.muted) {
margin: 0 0 2px;
text-indent: 2em;
text-align: justify;
}
table { width: 100%; border-collapse: collapse; margin-top: 4px; }
td { border: 1px solid #999; padding: 3px; vertical-align: top; font-size: 10pt; }
.muted { color: #666; font-size: 10pt; }
.compact { line-height: 1.35; }
/* 末尾签章区:左客户签名,右服务商/签章/公章(表格布局,兼容 PDF */
.sign-area {
margin-top: 50px;
padding-top: 8px;
border-top: 1px solid #ccc;
width: 100%;
page-break-inside: avoid;
}
.sign-table {
margin-top: 80px;
width: 100%;
border-collapse: collapse;
border: none;
}
.sign-table td {
border: none;
padding: 0;
vertical-align: top;
}
.sign-left {
width: 48%;
text-align: left;
padding-right: 24px;
vertical-align: middle;
}
.sign-right {
width: 52%;
text-align: right;
padding-left: 24px;
vertical-align: middle;
}
.sign-left-inner {
border: none;
border-collapse: collapse;
width: auto;
margin: 0;
}
.sign-left-inner td {
border: none;
padding: 0 8px 0 0;
vertical-align: middle;
}
.sign-left-inner .sign-label-cell { font-weight: 700; font-size: 11pt; white-space: nowrap; }
.sign-left-inner .sign-img-cell { padding-right: 0; }
.sign-box {
display: inline-block;
text-align: left;
min-width: 240px;
}
.sign-row {
position: relative;
margin-top: 6px;
font-size: 11pt;
}
.sign-label {
display: inline-block;
width: 64px;
font-weight: 700;
}
.sign-line {
display: inline-block;
width: 172px;
border-bottom: 1px solid #111;
height: 14px;
vertical-align: bottom;
}
.seal-img {
position: absolute;
left: 100px;
bottom: -6px;
width: 120px;
opacity: 0.92;
}
</style>
</head>
<body>
<!-- 水印:每页一个,居中显示 -->
<div class="watermark-layer">
<div class="wm">途安伴旅</div>
</div>
<div class="content">
<h1 style="padding-top: 0; margin-top: 0">途安出行+保障服务凭证</h1>
<div class="meta">
<div class="row"><span class="label">凭证编号:</span><span th:text="${certificateNo}">TABLA20260101001</span></div>
</div>
<div class="section">
<div class="title">合同条款:</div>
<p>
途安出行+保障服务产品为龙岩途安伴旅汽车服务有限公司(以下简称“本公司”)和保险公司合作的一款保障类服务产品,途安出行+保障服务产品已向保险公司投递相关保险。
服务合同中双方约定的服务期限、承保范围、保障责任、除外责任、申请资料等内容如下:
</p>
</div>
<div class="section">
<div class="title">车主信息卡及服务声明(摘录):</div>
<table>
<tr>
<td>行驶证车主</td><td th:text="${order.carBuyer} ?: '—'"></td>
<td>证件号码</td><td th:text="${order.certNo} ?: '—'"></td>
</tr>
<tr>
<td>车辆品牌</td><td th:text="${order.carBrand} ?: '—'"></td>
<td>联系电话</td><td th:text="${order.mobile} ?: '—'"></td>
</tr>
<tr>
<td>车架号(VIN)</td><td th:text="${order.vin} ?: '—'"></td>
<td>发动机号</td><td th:text="${order.engineNo} ?: '—'"></td>
</tr>
<tr>
<td>购车发票价</td><td th:text="${order.invoiceAmount} ?: '—'"></td>
<td>车牌号码</td><td th:text="${order.licensePlate} ?: '—'"></td>
</tr>
<tr>
<td>车主联系地址</td><td colspan="3" th:text="${order.contactAddress} ?: '—'"></td>
</tr>
<tr>
<td>服务经销商</td><td th:text="${order.storeName} ?: '—'"></td>
<td>服务费</td><td th:text="${order.productFee} ?: '—'"></td>
</tr>
</table>
</div>
<div class="section">
<div class="title">一、服务期限:</div>
<p>3年<span th:text="${serviceStartDate}">2026-01-16</span> 零时起至 <span th:text="${serviceEndDate}">2029-01-16</span> 二十四时止。</p>
</div>
<div class="section">
<div class="title">二、服务范围:</div>
<p>
中华人民共和国境内(港、澳、台除外),7座及以下乘用车(非营运车)。燃油车车龄3个月内新能源车龄6个月内的新车不承保次新车、二手车车龄以行驶证初登日期计算。
a. 超出车龄约定不予承保b. 车辆购买时不存在重大事故;车辆的性能、动力没有被改装过;车辆非水泡、火烧车;
c. 车辆出险时已经全额购买机动车损失保险d.车辆必须按照厂家车辆使用手册、厂家说明书等相关规定进行使用;
e. 车辆使用性质为非公共服务用途车辆、非比赛竞赛用车。
</p>
</div>
<div class="section">
<div class="title">三、保障责任:</div>
<p>
在服务有效期内,当标的车辆遭遇机动车商业保险所保障的各类车损(仅限双方事故),包括车辆驾驶人在此次交通事故责任认定中属于全责、主要责任、同等责任、次要责任或无责任(需触发对方商业车险三者险责任且非零结案),
如标的车辆的车损在本次事故中由商业车险公司核定最终支付的定损金额达到标的车发票金额10%-30%,回原店维修可申请维修款补贴;
超过30%回原店维修则可通过原购买服务的门店申请维修款补贴或车辆置换服务(仅可选择一种赔偿方式)。
</p>
</div>
<div class="section">
<div class="title">四、补贴标准:</div>
<p>
权益一标的车发票金额×10%≤定损金额&lt;标的车发票金额×30%的,补贴金额=定损金额×10%的维修款补贴。服务期限2026年01月16日至2029年01月16日。<br/>
权益二定损金额≥标的车发票金额×30%,且非全损及推定全损时,如车主选择换新车,车主需签署《机动车置换服务合同》让渡原车及商业车险所有权并配合完成标的车辆过户工作,补贴金额=标的车发票金额+车辆购置税+车辆登记费。服务期限2026年01月16日至2026年01月16日。<br/>
权益三:标的车辆因意外事故导致全损的,补偿金额=置换新车产生的差价费用+车辆登记费。标的车辆因意外事故导致推定全损,补偿金额=车辆折旧金额+车辆登记费。具体补偿金额按以下补贴标准执行。服务期限2026年01月04日至2029年01月16日。<br/>
说明a. 服务期内标的车发票金额×10%≤定损金额&lt;标的车发票金额×30%的每年最多享受2次赔偿且限额5000元/次。(当触发定损金额≥标的车发票金额×30%及全损,补贴责任结束后服务终止)b. 置换新车产生的差价费用=标的车发票金额-车损险赔付金额;车辆折旧金额=标的车发票金额×车辆自购买本服务产品之日起计算的已使用月数×0.82%(月折旧率)不足一个月不纳入计算c. 车辆登记费包括新车注册登记过程中发生的关税、车船税、验车费、上牌费1000元为限等政府部门收费。购置税以车辆发票显示的购置税金额或标的车发票金额×购置税率为准车辆购置税金额及额外费用=车辆购置发票金额×8.55%上限触发全损及推定全损情况下上牌费、购置税不超过购车发票价二手车车辆商业险车损定价的7%d. 标的车发票金额/购置税金额与出险时重新购置同款车型新车发票价金额/购置税金额不一致的,以两者取低者为准进行赔付(因同款车型停售或车主主动要求置换其他车型时同样适用本原则e. 本服务凭证内所有权益触发赔付时每次赔付的绝对免赔额为300元/次;
备注:权益一、权益二、权益三仅限购买服务的服务期限内有效。针对同一次事故,或者多次事故同一次进场,都仅可选择一种保障服务且仅限一次赔付,当触发权益二或权益三换新车服务完成后,此项产品内的所有赔偿服务条款均已结束,所有定损金额以保险公司车损险维修定损金额为准。
</p>
</div>
<div class="section">
<div class="title">五、除外责任:</div>
<p>
a.当车辆全损或推定全损时,未能提供被保险车辆购买商业车险的承保公司,所提供全损或者推定全损的盖章协议或证明;
b.当车辆遭受损害事故仅导致部分损失时定损金额未达到标的车发票金额×10%的;
c.当车辆遭受损害事故仅导致部分损失时,未能提供相关车辆购买商业车险的承保公司出具的赔偿协议或证明的;
d.车辆的认证信息、车辆行驶证或实际使用性质与服务凭证中适用条件要求不一致的;
e.车辆所有人与服务凭证车主不一致的;
f.车辆遭受的损害事故发生在服务期限之外,或车主未在服务凭证规定的期限内提出新车置换或补贴要求;
g.任何形式的人身伤害、财产损失,及除本合同所列车辆置换费用以外其他任何费用支出;
h.车辆全损及购置新车的过程中所产生的任何间接损失、赔偿责任;
i.标的车辆驾驶员或车主的欺诈、不诚实、酒驾、毒驾等违法犯罪行为;
j.战争、敌对行为、军事行动、武装冲突、恐怖主义活动、罢工、暴动、骚乱;
k.盗抢、划痕(划痕险)、自燃水淹、自然灾害、核爆炸、核裂变、核聚变;
l.放射性污染及其他各种环境污染;
m.行政行为、司法行为。
</p>
</div>
<div class="section">
<div class="title">六、申请资料:</div>
<p>
发生保险责任事故时车主应配合提供相应索赔材料1索赔申请书2原购车发票原件3《车辆增值服务合同》原件4被保车辆行驶证正副页、车主身份证正反面5商业险保单6事故证明或交通事故陈述书或保险公司查勘报告如无则协商增加10%绝对免赔率7全损或推定全损协议及赔款水单截图8置换后新购车发票9新购车合格证10新购车购置税发票11上牌服务费发票12车架号照片13前后45度角整车照片14车辆损失照片15领款人信息姓名、账号、开户行16非被保险人收款需提供收款信息并签署权益转让协议17反洗钱资料单案赔款超1万元需提供相关身份资料。未达到全损或推定全损且未置换新车的无需提供第7—11项材料。
</p>
</div>
<div class="section">
<div class="title">七、特别约定:</div>
<p>
未经补贴接收方书面同意,本凭证及对应保单不得退保、批改(不影响补贴接收方权益的批改除外) ;如发生保障服务赔偿责任时,按补贴接收方的权益转让书支付赔款。 以上服务权益补贴服务必须到我司授权合作门店(优先原购买本服务门店)进行维修方可办理。如本车辆未购买商业车损险或商业险拒赔的则不能享受理赔服务。 服务期间内如发生车辆转让、过户、变更车辆使用性质,保障服务责任终止。
</p>
</div>
<div class="section">
<div class="title">八、特别声明:</div>
<p>
特别声明车主应当在24小时内向商业险保险公司及交管部门报案同时致电龙岩途安伴旅汽车服务有限公司全国统一客服热线18039881137 进行备案并通过龙岩途安伴旅汽车服务有限公司向保险公司索赔。服务保障正式生效日期在付款后即T+12日T为此项服务产品的付款购买日为车辆使用观察期此期限内发生的事故将不产生赔付责任后续为此项服务保障权益的正式生效日期付款行为以途安伴旅书面确认收到款项之日为准。如系分期客户若其未按合同/微信通知以及其他告知的约定按时支付分期款项经催告后在合理期限内仍未支付的合同终止且本公司在款项未结清期限内有权利拒绝赔偿和退还已支付款项。关于退费凭证生成日起3天扣除300元工本费无息退还剩余保费3天以上不支持退款已发生补贴责任的不可退费。
</p>
</div>
<!-- 末尾签章:左 客户签名+签名(左右排布、剧左),右 服务商+签章(剧右) -->
<div class="sign-area">
<table class="sign-table">
<tr>
<td class="sign-left">
<table class="sign-left-inner" cellpadding="0" cellspacing="0">
<tr>
<td class="sign-label-cell">客户签名:</td>
<td class="sign-img-cell">
<img th:if="${customerSignatureBase64 != null and customerSignatureBase64 != ''}"
th:src="${customerSignatureBase64}"
alt="客户签名"
style="max-width: 160px; max-height: 72px; border: 1px solid #ddd; background: #fff;" />
</td>
</tr>
</table>
</td>
<td class="sign-right">
<div class="sign-box">
<div class="sign-row"><span class="sign-label">服务商:</span><span class="sign-line"></span></div>
<div class="sign-row"><span class="sign-label">签章:</span><span class="sign-line"></span><img class="seal-img" th:if="${sealImageBase64 != null and sealImageBase64 != ''}" th:src="${sealImageBase64}" alt="seal" /></div>
</div>
</td>
</tr>
</table>
</div>
</div> <!-- /content -->
</body>
</html>

View File

@@ -136,6 +136,7 @@ public class RenewalOrderServiceImplTest extends BaseDbUnitTest {
o.setRemark(null);
o.setInputUser(null);
o.setContractRemark(null);
o.setContractUrl(null);
o.setCreateTime(null);
});
renewalOrderMapper.insert(dbRenewalOrder);
@@ -191,6 +192,7 @@ public class RenewalOrderServiceImplTest extends BaseDbUnitTest {
renewalOrderMapper.insert(cloneIgnoreId(dbRenewalOrder, o -> o.setInputUser(null)));
// 测试 contractRemark 不匹配
renewalOrderMapper.insert(cloneIgnoreId(dbRenewalOrder, o -> o.setContractRemark(null)));
renewalOrderMapper.insert(cloneIgnoreId(dbRenewalOrder, o -> o.setContractUrl(null)));
// 测试 createTime 不匹配
renewalOrderMapper.insert(cloneIgnoreId(dbRenewalOrder, o -> o.setCreateTime(null)));
// 准备参数
@@ -221,6 +223,7 @@ public class RenewalOrderServiceImplTest extends BaseDbUnitTest {
reqVO.setRemark(null);
reqVO.setInputUser(null);
reqVO.setContractRemark(null);
reqVO.setContractUrl(null);
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
// 调用