This commit is contained in:
2026-03-02 09:23:23 +08:00
parent 572e9a443f
commit 2eceb44a7f
19 changed files with 622 additions and 26 deletions

View File

@@ -93,5 +93,11 @@
<artifactId>yudao-module-infra-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -77,6 +77,27 @@ public class RenewalOrderController {
return success(renewalOrderService.generateContractHtml(id));
}
@PostMapping("/create-sign-token")
@Operation(summary = "创建线上签名令牌")
@Parameter(name = "id", description = "订单编号", required = true)
@PreAuthorize("@ss.hasPermission('car:renewal-order:query')")
public CommonResult<Map<String, String>> createSignToken(@RequestParam("id") Long id) {
String uuid = renewalOrderService.createSignToken(id);
Map<String, String> result = new HashMap<>();
result.put("uuid", uuid);
result.put("signUrl", "/admin-api/car/sign/" + uuid);
return success(result);
}
@PostMapping("/clear-contract-sign")
@Operation(summary = "清空订单合同与客户签名")
@Parameter(name = "id", description = "订单编号", required = true)
@PreAuthorize("@ss.hasPermission('car:renewal-order:update')")
public CommonResult<Boolean> clearContractAndSignature(@RequestParam("id") Long id) {
renewalOrderService.clearContractAndSignature(id);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除车辆续保订单")
@Parameter(name = "id", description = "编号", required = true)

View File

@@ -74,7 +74,8 @@ public class RenewalOrderSaveReqVO {
@Schema(description = "产品时效")
private String productValidity;
@Schema(description = "原厂质保时长")
@Schema(description = "产品年限(原厂质保时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
@NotBlank(message = "产品年限不能为空")
private String originalWarrantyYears;
@Schema(description = "原厂质保里程")

View File

@@ -24,6 +24,9 @@ public class RenewalProductPageReqVO extends PageParam {
@Schema(description = "产品类别car_renewal_product_type00 无忧01 延保)", example = "2")
private String productType;
@Schema(description = "生效年限car_renewal_year", example = "1")
private String effectiveYear;
@Schema(description = "备注", example = "随便")
private String remark;

View File

@@ -31,6 +31,11 @@ public class RenewalProductRespVO {
@DictFormat("car_renewal_product_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private String productType;
@Schema(description = "生效年限car_renewal_year", example = "1")
@ExcelProperty(value = "生效年限", converter = DictConvert.class)
@DictFormat("car_renewal_year")
private String effectiveYear;
@Schema(description = "备注", example = "随便")
@ExcelProperty("备注")
private String remark;

View File

@@ -23,6 +23,9 @@ public class RenewalProductSaveReqVO {
@NotEmpty(message = "产品类别car_renewal_product_type00 无忧01 延保)不能为空")
private String productType;
@Schema(description = "生效年限car_renewal_year", example = "1")
private String effectiveYear;
@Schema(description = "备注", example = "随便")
private String remark;

View File

@@ -0,0 +1,158 @@
package cn.iocoder.yudao.module.car.controller.admin.sign;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.car.dal.redis.OnlineSignRedisDAO;
import cn.iocoder.yudao.module.car.service.renewalorder.RenewalOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 线上签名控制器(公开接口,无需登录)
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - 线上签名(公开)")
@Controller
@RequestMapping("/admin-api/car/sign")
@Slf4j
@PermitAll
public class OnlineSignController {
@Resource
private OnlineSignRedisDAO onlineSignRedisDAO;
@Resource
private RenewalOrderService renewalOrderService;
/**
* 签名成功页GET 返回 HTML可选 contractUrl 用于“查看合同”链接)
*/
@GetMapping("/success")
@Operation(summary = "签名成功页")
public String signSuccess(@RequestParam(value = "contractUrl", required = false) String contractUrl, Model model) {
model.addAttribute("contractUrl", contractUrl != null && !contractUrl.isEmpty() ? contractUrl : null);
return "sign/sign-success";
}
/**
* 签名页面GET 返回 HTML
*/
@GetMapping("/{uuid}")
@Operation(summary = "线上签名页面")
@Parameter(name = "uuid", description = "签名令牌", required = true)
public String signPage(@PathVariable("uuid") String uuid, Model model) {
String value = onlineSignRedisDAO.get(uuid);
if (value == null || value.isEmpty()) {
model.addAttribute("valid", false);
model.addAttribute("message", "当前在线签名实效已过期,请重新分享在线签名");
return "sign/sign-expired";
}
String[] parts = value.split(":", 2);
if (parts.length != 2) {
model.addAttribute("valid", false);
model.addAttribute("message", "当前在线签名实效已过期,请重新分享在线签名");
return "sign/sign-expired";
}
model.addAttribute("valid", true);
model.addAttribute("uuid", uuid);
return "sign/sign-page";
}
/**
* 获取合同 HTML用于签名页预览
*/
@GetMapping("/{uuid}/contract")
@Operation(summary = "获取合同HTML")
@ResponseBody
@Parameter(name = "uuid", description = "签名令牌", required = true)
public CommonResult<String> getContract(@PathVariable("uuid") String uuid) {
String value = onlineSignRedisDAO.get(uuid);
if (value == null || value.isEmpty()) {
return CommonResult.error(404, "当前在线签名实效已过期,请重新分享在线签名");
}
String[] parts = value.split(":", 2);
if (parts.length != 2) {
return CommonResult.error(404, "当前在线签名实效已过期,请重新分享在线签名");
}
try {
Long tenantId = Long.parseLong(parts[0]);
Long orderId = Long.parseLong(parts[1]);
String html = TenantUtils.execute(tenantId, () -> renewalOrderService.generateContractHtml(orderId));
return success(html);
} catch (Exception e) {
log.error("获取合同HTML失败, uuid={}", uuid, e);
return CommonResult.error(500, "获取合同失败");
}
}
/**
* 校验令牌是否有效
*/
@GetMapping("/{uuid}/validate")
@Operation(summary = "校验签名令牌")
@ResponseBody
@Parameter(name = "uuid", description = "签名令牌", required = true)
public CommonResult<Boolean> validate(@PathVariable("uuid") String uuid) {
String value = onlineSignRedisDAO.get(uuid);
boolean valid = value != null && !value.isEmpty() && value.contains(":");
return success(valid);
}
/**
* 提交签名并生成合同
*/
@PostMapping("/{uuid}/submit")
@Operation(summary = "提交签名并生成合同")
@ResponseBody
@Parameter(name = "uuid", description = "签名令牌", required = true)
public CommonResult<Map<String, Object>> submitSignature(
@PathVariable("uuid") String uuid,
@RequestBody Map<String, String> body) {
String signatureBase64 = body != null ? body.get("signature") : null;
if (signatureBase64 == null || signatureBase64.isEmpty()) {
return CommonResult.error(400, "签名不能为空");
}
String value = onlineSignRedisDAO.get(uuid);
if (value == null || value.isEmpty()) {
return CommonResult.error(404, "当前在线签名实效已过期,请重新分享在线签名");
}
String[] parts = value.split(":", 2);
if (parts.length != 2) {
return CommonResult.error(404, "当前在线签名实效已过期,请重新分享在线签名");
}
try {
Long tenantId = Long.parseLong(parts[0]);
Long orderId = Long.parseLong(parts[1]);
String contractUrl = TenantUtils.execute(tenantId, () -> {
renewalOrderService.submitOnlineSignature(orderId, signatureBase64);
return renewalOrderService.generateContract(orderId);
});
// 签名成功后删除令牌,防止重复使用
onlineSignRedisDAO.delete(uuid);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("contractUrl", contractUrl);
result.put("message", "签名成功,合同已生成");
return success(result);
} catch (Exception e) {
log.error("提交签名失败, uuid={}", uuid, e);
return CommonResult.error(500, "提交签名失败: " + e.getMessage());
}
}
}

View File

@@ -41,6 +41,10 @@ public class RenewalProductDO extends BaseDO {
* 枚举 {@link TODO car_renewal_product_type 对应的类}
*/
private String productType;
/**
* 生效年限car_renewal_year
*/
private String effectiveYear;
/**
* 备注
*/

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.car.dal.redis;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* 线上签名 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class OnlineSignRedisDAO {
private static final Duration EXPIRE = Duration.ofHours(12);
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 存储签名令牌uuid -> tenantId:orderId
*
* @param uuid 唯一序列码
* @param value 租户ID:订单ID
*/
public void set(String uuid, String value) {
String key = RedisKeyConstants.SIGN_TOKEN + uuid;
stringRedisTemplate.opsForValue().set(key, value, EXPIRE);
}
/**
* 获取签名令牌对应的值
*
* @param uuid 唯一序列码
* @return 租户ID:订单ID不存在返回 null
*/
public String get(String uuid) {
String key = RedisKeyConstants.SIGN_TOKEN + uuid;
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除签名令牌(签名成功后删除,防止重复使用)
*
* @param uuid 唯一序列码
*/
public void delete(String uuid) {
String key = RedisKeyConstants.SIGN_TOKEN + uuid;
stringRedisTemplate.delete(key);
}
}

View File

@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.car.dal.redis;
/**
* 车辆模块 Redis Key 常量
*
* @author 芋道源码
*/
public interface RedisKeyConstants {
/**
* 线上签名令牌
* KEY 格式car:sign:token:{uuid}
* VALUE 格式tenantId:orderId加密后的订单ID
* 过期时间12小时
*/
String SIGN_TOKEN = "car:sign:token:";
}

View File

@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.car.service.contract;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalorder.RenewalOrderDO;
import cn.iocoder.yudao.module.car.dal.dataobject.renewalproduct.RenewalProductDO;
import cn.iocoder.yudao.module.car.service.renewalproduct.RenewalProductService;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import cn.hutool.core.util.StrUtil;
@@ -39,6 +41,9 @@ public class ContractServiceImpl implements ContractService {
@Resource
private FileService fileService;
@Resource
private RenewalProductService renewalProductService;
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";
@@ -160,9 +165,43 @@ public class ContractServiceImpl implements ContractService {
String certificateNo = generateCertificateNo(renewalOrder);
context.setVariable("certificateNo", certificateNo);
// 服务期限
// 服务期限:根据产品生效年限计算
LocalDate startDate = LocalDate.now();
LocalDate endDate = startDate.plusYears(3);
int effectiveYears = 3; // 默认3年
// 根据订单的产品ID查询产品信息获取生效年限
if (renewalOrder.getProductId() != null) {
try {
RenewalProductDO product = renewalProductService.getRenewalProduct(renewalOrder.getProductId());
if (product != null && product.getEffectiveYear() != null && !product.getEffectiveYear().isEmpty()) {
try {
// 将字典值转换为年数(字典值可能是 "1", "2", "3" 等)
effectiveYears = Integer.parseInt(product.getEffectiveYear());
log.info("订单 {} 的产品 {} 生效年限: {} 年", renewalOrder.getId(), renewalOrder.getProductId(), effectiveYears);
} catch (NumberFormatException e) {
log.warn("产品生效年限格式不正确: {}, 使用默认值3年", product.getEffectiveYear(), e);
effectiveYears = 3;
}
} else {
log.warn("订单 {} 的产品 {} 未设置生效年限使用默认值3年", renewalOrder.getId(), renewalOrder.getProductId());
}
} catch (Exception e) {
log.error("查询产品信息失败订单ID: {}, 产品ID: {}, 使用默认值3年", renewalOrder.getId(), renewalOrder.getProductId(), e);
effectiveYears = 3;
}
} else {
log.warn("订单 {} 未关联产品使用默认值3年", renewalOrder.getId());
}
// 计算权益一、权益二的结束日期最多3年
int benefit12Years = Math.min(effectiveYears, 3);
LocalDate benefit12EndDate = startDate.plusYears(benefit12Years);
// 计算权益三的结束日期(使用实际年限)
LocalDate benefit3EndDate = startDate.plusYears(effectiveYears);
// 服务期限(用于合同主体部分)
LocalDate endDate = benefit3EndDate; // 使用实际年限作为合同总期限
context.setVariable("serviceStartDate", startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
context.setVariable("serviceEndDate", endDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
@@ -170,6 +209,17 @@ public class ContractServiceImpl implements ContractService {
context.setVariable("serviceStartDateText", startDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
context.setVariable("serviceEndDateText", endDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
// 权益一、权益二的日期最多3年
context.setVariable("benefit12StartDateText", startDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
context.setVariable("benefit12EndDateText", benefit12EndDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
// 权益三的日期(使用实际年限)
context.setVariable("benefit3StartDateText", startDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
context.setVariable("benefit3EndDateText", benefit3EndDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
// 服务年限(用于模板中显示年限)
context.setVariable("serviceYears", effectiveYears);
// 签章图片路径(转换为 base64
// 注意openhtmltopdf 可能不支持 WebP 格式,如果出错会跳过签章显示
try {

View File

@@ -68,4 +68,27 @@ public interface RenewalOrderService {
*/
String generateContractHtml(Long id);
/**
* 创建线上签名令牌
*
* @param orderId 订单编号
* @return uuid 签名链接序列码
*/
String createSignToken(Long orderId);
/**
* 提交线上签名(上传签名图片并更新订单)
*
* @param orderId 订单编号
* @param signatureBase64 签名 base64 数据
*/
void submitOnlineSignature(Long orderId, String signatureBase64);
/**
* 清空订单的合同与客户签名(用于重新生成合同时先清空再让客户扫码签名)
*
* @param id 订单编号
*/
void clearContractAndSignature(Long id);
}

View File

@@ -14,7 +14,13 @@ 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.dal.redis.OnlineSignRedisDAO;
import cn.iocoder.yudao.module.car.service.contract.ContractService;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import java.util.Base64;
import java.util.UUID;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.*;
@@ -35,6 +41,12 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
@Resource
private ContractService contractService;
@Resource
private OnlineSignRedisDAO onlineSignRedisDAO;
@Resource
private FileApi fileApi;
@Override
public Long createRenewalOrder(RenewalOrderSaveReqVO createReqVO) {
// 插入
@@ -107,4 +119,51 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
}
return contractService.generateContractHtml(order);
}
@Override
public String createSignToken(Long orderId) {
validateRenewalOrderExists(orderId);
String uuid = UUID.randomUUID().toString().replace("-", "");
Long tenantId = TenantContextHolder.getTenantId();
String value = (tenantId != null ? tenantId : 0) + ":" + orderId;
onlineSignRedisDAO.set(uuid, value);
log.info("创建线上签名令牌 - orderId: {}, uuid: {}", orderId, uuid);
return uuid;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitOnlineSignature(Long orderId, String signatureBase64) {
RenewalOrderDO existOrder = renewalOrderMapper.selectById(orderId);
if (existOrder == null) {
throw exception(RENEWAL_ORDER_NOT_EXISTS);
}
try {
byte[] imageBytes = Base64.getDecoder().decode(signatureBase64.replaceAll("^data:image/\\w+;base64,", ""));
String path = "signature/" + orderId + "_" + System.currentTimeMillis() + ".png";
String url = fileApi.createFile("signature.png", path, imageBytes);
RenewalOrderDO update = new RenewalOrderDO();
update.setId(orderId);
update.setCustomerSignatureUrl(url);
renewalOrderMapper.updateById(update);
log.info("线上签名已保存 - orderId: {}, url: {}", orderId, url);
} catch (Exception e) {
log.error("上传签名图片失败", e);
throw new RuntimeException("上传签名失败: " + e.getMessage(), e);
}
}
@Override
public void clearContractAndSignature(Long id) {
RenewalOrderDO order = renewalOrderMapper.selectById(id);
if (order == null) {
throw exception(RENEWAL_ORDER_NOT_EXISTS);
}
RenewalOrderDO update = new RenewalOrderDO();
update.setId(id);
update.setContractUrl(null);
update.setCustomerSignatureUrl(null);
renewalOrderMapper.updateById(update);
log.info("已清空订单合同与签名 - orderId: {}", id);
}
}

View File

@@ -2,7 +2,7 @@
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>途安出行+保障服务凭证</title>
<title>途安出行保障服务凭证</title>
<style>
/* ======================
* 合同标准排版OpenHTMLToPDF
@@ -162,7 +162,7 @@
<div class="wm">途安伴旅</div>
</div>
<div class="content">
<h1 style="padding-top: 0; margin-top: 0">途安出行+保障服务凭证</h1>
<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>
@@ -171,7 +171,7 @@
<div class="section">
<div class="title">合同条款:</div>
<p>
途安出行+保障服务产品为龙岩途安伴旅汽车服务有限公司(以下简称“本公司”)和保险公司合作的一款保障类服务产品,途安出行+保障服务产品已向保险公司投递相关保险。
途安出行保障服务产品为龙岩途安伴旅汽车服务有限公司(以下简称“本公司”)和保险公司合作的一款保障类服务产品,途安出行+保障服务产品已向保险公司投递相关保险。
服务合同中双方约定的服务期限、承保范围、保障责任、除外责任、申请资料等内容如下:
</p>
</div>
@@ -207,7 +207,7 @@
<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>
<p><span th:text="${serviceYears}">3</span>年,自 <span th:text="${serviceStartDate}">xxxx-xx-xx</span> 零时起至 <span th:text="${serviceEndDate}">xxxx-xx-xx</span> 二十四时止。</p>
</div>
<div class="section">
@@ -231,12 +231,11 @@
<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 style="text-indent: 0em;">
权益一:标的车发票金额x10%≤定损金额&lt;标的车发票金额x30%的,补贴金额=定损金额x10%的维修款补贴。服务期限:<span th:text="${benefit12StartDateText}">2026年01月16日</span><span th:text="${benefit12EndDateText}">2029年01月16日</span><br/>
权益二定损金额≥标的车发票金额×30%,且非全损及推定全损时,如车主选择换新车,车主需签署《机动车置换服务合同》让渡原车及商业车险所有权并配合完成标的车辆过户工作,补贴金额=标的车发票金额+车辆购置税+车辆登记费。服务期限:<span th:text="${benefit12StartDateText}">2026年01月16日</span><span th:text="${benefit12EndDateText}">2029年01月16日</span><br/>
权益三:标的车辆因双方事故造成的全损,且车主回店置换同品牌新车,补偿金额=置换新车产生的差价费用+车辆登记费标的车辆因双方事故造成推定全损的,且车主回店置换同品牌新车,补偿金额=车辆折旧金额+车辆登记费。服务期限:<span th:text="${benefit3StartDateText}">xxxx年xx月xx日</span><span th:text="${benefit3EndDateText}">xxxx年xx月xx日</span>补偿总金额根据此项服务合同生效日开始计算服务期限前三年内以新车或旧车购置价40%为限服务期限后两年内以新车或旧车购置价30%为限均以其低者为准。具体补偿金额按以下补贴标准执行。说明a. 服务期内标的车发票金额×10%≤定损金额&lt;标的车发票金额×30%的每年最多享受2次赔偿且限额5000元/次。(当触发定损金额≥标的车发票金额×30%及全损,补贴责任结束后服务终止)
总3页b. 置换新车产生的差价费用=标的车发票金额-车损险赔付金额;车辆折旧金额=标的车发票金额×车辆自购买本服务产品之日起计算的已使用月数×0.82%(月折旧率),不足一个月不纳入计算; c. 车辆登记费包括新车注册登记过程中发生的关税、车船税、验车费、上牌费1000元为限等政府部门收费。购置税以车辆发票显示的购置税金额或标的车发票金额×购置税率为准 车辆购置税金额及额外费用=车辆购置发票金额×8.55%上限触发全损及推定全损情况下上牌费、购置税不超过购车发票价二手车车辆商业险车损定价的7%d. 标的车发票金额/购置税金额与出险时重新购置同款车型新车发票价金额/购置税金额不一致的,以两者取低者为准进行赔付(因同款车型停售或车主主动要求置换其他车型时同样适用本原则e.本服务凭证内所有权益触发赔付时每次赔付的绝对免赔额为300元/次。其他备注:权益一 、权益二仅限购买服务的1-3年期限内有效权益三依照其限定条件在整个服务周期内均有效针对同一次事故或者多次事故同一次进场都仅可选择一种保障服务且仅限一次赔付当触发权益二、权益三任何一项换新车服务完成后此项产品内的所有赔偿服务条款均已结束定损金额以保险公司车损险维修定损金额为准。
</p>
</div>
@@ -276,7 +275,7 @@
<div class="section">
<div class="title">八、特别声明:</div>
<p>
特别声明:车主应当在24小时内向商业险保险公司及交管部门报案同时致电龙岩途安伴旅汽车服务有限公司全国统一客服热线18039881137 进行备案并通过龙岩途安伴旅汽车服务有限公司向保险公司索赔。服务保障正式生效日期在付款后即T+12日T为此项服务产品的付款购买日为车辆使用观察期此期限内发生的事故将不产生赔付责任后续为此项服务保障权益的正式生效日期付款行为以途安伴旅书面确认收到款项之日为准。如系分期客户若其未按合同/微信通知以及其他告知的约定,按时支付分期款项,经催告后在合理期限内仍未支付的,合同终止,且本公司在款项未结清期限内有权利拒绝赔偿和退还已支付款项关于退费凭证生成日起3天扣除300元工本费无息退还剩余保费3天以上不支持退款已发生补贴责任的不可退费。
车主应当在24小时内向商业险保险公司及交管部门报案同时致电龙岩途安伴旅汽车服务有限公司全国统一客服热线18039881137 进行备案,并通过龙岩途安伴旅汽车服务有限公司向保险公司索赔。服务保障正式生效日总3页在付款后即T+12日T为此项服务产品的付款购买日为车辆使用观察期此期限内发生的事故将不产生赔付责任后续为此项服务保障权益的正式生效日期付款行为以途安伴旅书面确认收到款项之日为准。如系分期客户若其未按相关告知的约定,按时支付分期款项,经催告后在合理期限内仍未支付的,合同终止,且本公司在款项未结清期限内有权利拒绝赔偿和退还已支付款项 关于退费凭证生成日起3天扣除500元工本费无息退还剩余保费3天以上不支持退款已发生补贴责任的不可退费。
</p>
</div>
<!-- 末尾签章:左 客户签名+签名(左右排布、剧左),右 服务商+签章(剧右) -->

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>在线签名已过期</title>
<style>
body { font-family: "Microsoft YaHei", "SimSun", sans-serif; margin: 0; padding: 40px; text-align: center; }
.message { font-size: 18px; color: #e6a23c; margin: 60px 0; }
</style>
</head>
<body>
<div class="message" th:text="${message}">当前在线签名实效已过期,请重新分享在线签名</div>
</body>
</html>

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>线上签名 - 途安出行保障服务</title>
<style>
* { box-sizing: border-box; }
body { font-family: "Microsoft YaHei", "SimSun", sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); }
h1 { font-size: 20px; margin: 0 0 20px; text-align: center; }
.contract-box { border: 1px solid #ddd; padding: 16px; margin-bottom: 24px; max-height: 400px; overflow-y: auto; font-size: 12px; line-height: 1.5; }
.sign-area { margin: 20px 0; }
.sign-area label { display: block; margin-bottom: 8px; font-weight: bold; }
#signCanvas { border: 1px solid #999; border-radius: 4px; background: #fff; touch-action: none; }
.btn-group { margin-top: 20px; display: flex; gap: 12px; justify-content: center; }
.btn { padding: 10px 24px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; }
.btn-primary { background: #409eff; color: #fff; }
.btn-primary:disabled { background: #a0cfff; cursor: not-allowed; }
.btn-default { background: #f0f0f0; color: #333; }
.loading { text-align: center; padding: 40px; color: #909399; }
.error { color: #f56c6c; padding: 12px; margin: 12px 0; }
.success { color: #67c23a; padding: 12px; margin: 12px 0; }
</style>
</head>
<body>
<div class="container">
<h1>途安出行保障服务 - 线上签名</h1>
<div id="contractContainer" class="contract-box loading">正在加载合同内容...</div>
<div class="sign-area">
<label>请在此处签名:</label>
<canvas id="signCanvas" width="600" height="200"></canvas>
<button type="button" class="btn btn-default" onclick="clearSign()" style="margin-top:8px;">清空签名</button>
</div>
<div id="msgBox"></div>
<div class="btn-group">
<button type="button" class="btn btn-primary" id="submitBtn" onclick="submitSign()" disabled>确认签名并生成合同</button>
</div>
</div>
<script th:inline="javascript">
const uuid = /*[[${uuid}]]*/ '';
const origin = window.location.origin;
const apiBase = origin + '/admin-api/car/sign/' + uuid;
let signCanvas, ctx, drawing = false, hasSignature = false;
window.onload = function() {
loadContract();
initCanvas();
};
function loadContract() {
fetch(apiBase + '/contract')
.then(r => r.json())
.then(res => {
if (res.code === 0 && res.data) {
document.getElementById('contractContainer').className = 'contract-box';
document.getElementById('contractContainer').innerHTML = res.data;
} else {
document.getElementById('contractContainer').className = 'contract-box error';
document.getElementById('contractContainer').innerHTML = res.msg || '加载合同失败';
}
})
.catch(err => {
document.getElementById('contractContainer').className = 'contract-box error';
document.getElementById('contractContainer').innerHTML = '当前在线签名实效已过期,请重新分享在线签名';
});
}
function initCanvas() {
signCanvas = document.getElementById('signCanvas');
ctx = signCanvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
signCanvas.addEventListener('mousedown', startDraw);
signCanvas.addEventListener('mousemove', draw);
signCanvas.addEventListener('mouseup', endDraw);
signCanvas.addEventListener('mouseleave', endDraw);
signCanvas.addEventListener('touchstart', handleTouchStart);
signCanvas.addEventListener('touchmove', handleTouchMove);
signCanvas.addEventListener('touchend', endDraw);
}
function getPos(e) {
const rect = signCanvas.getBoundingClientRect();
const scaleX = signCanvas.width / rect.width;
const scaleY = signCanvas.height / rect.height;
if (e.touches) {
return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY };
}
return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY };
}
function handleTouchStart(e) { e.preventDefault(); startDraw(e); }
function handleTouchMove(e) { e.preventDefault(); draw(e); }
function startDraw(e) { drawing = true; const p = getPos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); }
function draw(e) { if (!drawing) return; const p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSignature = true; document.getElementById('submitBtn').disabled = false; }
function endDraw() { drawing = false; }
function clearSign() {
ctx.clearRect(0, 0, signCanvas.width, signCanvas.height);
hasSignature = false;
document.getElementById('submitBtn').disabled = true;
}
function showMsg(text, isError) {
const box = document.getElementById('msgBox');
box.className = isError ? 'error' : 'success';
box.textContent = text;
}
function submitSign() {
if (!hasSignature) {
showMsg('请先完成签名', true);
return;
}
const dataUrl = signCanvas.toDataURL('image/png');
document.getElementById('submitBtn').disabled = true;
document.getElementById('submitBtn').textContent = '提交中...';
fetch(apiBase + '/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: dataUrl })
})
.then(r => r.json())
.then(res => {
if (res.code === 0 && res.data && res.data.success) {
var contractUrl = (res.data && res.data.contractUrl) ? encodeURIComponent(res.data.contractUrl) : '';
var successUrl = origin + '/admin-api/car/sign/success' + (contractUrl ? '?contractUrl=' + contractUrl : '');
window.location.href = successUrl;
return;
} else {
showMsg(res.msg || '提交失败', true);
document.getElementById('submitBtn').disabled = false;
document.getElementById('submitBtn').textContent = '确认签名并生成合同';
}
})
.catch(err => {
showMsg('提交失败: ' + (err.message || '网络错误'), true);
document.getElementById('submitBtn').disabled = false;
document.getElementById('submitBtn').textContent = '确认签名并生成合同';
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>签名成功 - 途安出行保障服务</title>
<style>
* { box-sizing: border-box; }
body { font-family: "Microsoft YaHei", "SimSun", sans-serif; margin: 0; padding: 20px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.container { max-width: 480px; width: 100%; margin: 0 auto; background: #fff; padding: 40px 24px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); text-align: center; }
.icon-success { width: 72px; height: 72px; margin: 0 auto 20px; background: #67c23a; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.icon-success::after { content: '✓'; font-size: 40px; color: #fff; font-weight: bold; }
h1 { font-size: 22px; margin: 0 0 12px; color: #303133; }
.desc { font-size: 14px; color: #909399; margin-bottom: 24px; line-height: 1.6; }
.btn { display: inline-block; padding: 10px 24px; border-radius: 4px; font-size: 14px; text-decoration: none; cursor: pointer; border: none; }
.btn-primary { background: #409eff; color: #fff; margin-right: 12px; }
.btn-default { background: #f0f0f0; color: #333; }
.btn-group { margin-top: 8px; }
</style>
</head>
<body>
<div class="container">
<div class="icon-success"></div>
<h1>签名成功</h1>
<p class="desc">合同已生成,您可关闭本页面。</p>
<div class="btn-group">
<a th:if="${contractUrl}" th:href="${contractUrl}" target="_blank" class="btn btn-primary">查看合同</a>
<button type="button" class="btn btn-default" onclick="window.close(); if (!window.closed) history.back();">关闭页面</button>
</div>
</div>
</body>
</html>

View File

@@ -54,17 +54,9 @@ spring:
primary: master
datasource:
master:
# MySQL Connector/J 8.X 连接的示例
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例
# url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
# url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例
# url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:mysql://47.98.192.5:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
username: root
password: root
password: mysql_BcMnw5
#url: jdbc:mysql://47.98.192.5:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
#username: root
@@ -86,9 +78,9 @@ spring:
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
host: 123.57.150.179 # 地址
host: 47.98.192.5 # 地址
port: 6379 # 端口
database: 0 # 数据库索引
database: 1 # 数据库索引
password: redis_pd2R8B # 密码,建议生产环境开启
--- #################### 定时任务相关配置 ####################

View File

@@ -209,6 +209,7 @@ yudao:
security:
permit-all_urls:
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
- /admin-api/car/sign/** # 线上签名页面及接口,无需登录
websocket:
enable: true # websocket的开关
path: /infra/ws # 路径
@@ -246,6 +247,7 @@ yudao:
- /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
- /jmreport/* # 积木报表,无法携带租户编号
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
- /admin-api/car/sign/** # 线上签名页面及接口,无法携带租户编号
ignore-tables:
- system_tenant
- system_tenant_package