修改
This commit is contained in:
@@ -93,5 +93,11 @@
|
|||||||
<artifactId>yudao-module-infra-biz</artifactId>
|
<artifactId>yudao-module-infra-biz</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -77,6 +77,27 @@ public class RenewalOrderController {
|
|||||||
return success(renewalOrderService.generateContractHtml(id));
|
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")
|
@DeleteMapping("/delete")
|
||||||
@Operation(summary = "删除车辆续保订单")
|
@Operation(summary = "删除车辆续保订单")
|
||||||
@Parameter(name = "id", description = "编号", required = true)
|
@Parameter(name = "id", description = "编号", required = true)
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ public class RenewalOrderSaveReqVO {
|
|||||||
@Schema(description = "产品时效")
|
@Schema(description = "产品时效")
|
||||||
private String productValidity;
|
private String productValidity;
|
||||||
|
|
||||||
@Schema(description = "原厂质保时长")
|
@Schema(description = "产品年限(原厂质保时长)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||||
|
@NotBlank(message = "产品年限不能为空")
|
||||||
private String originalWarrantyYears;
|
private String originalWarrantyYears;
|
||||||
|
|
||||||
@Schema(description = "原厂质保里程")
|
@Schema(description = "原厂质保里程")
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public class RenewalProductPageReqVO extends PageParam {
|
|||||||
@Schema(description = "产品类别(car_renewal_product_type:00 无忧,01 延保)", example = "2")
|
@Schema(description = "产品类别(car_renewal_product_type:00 无忧,01 延保)", example = "2")
|
||||||
private String productType;
|
private String productType;
|
||||||
|
|
||||||
|
@Schema(description = "生效年限(car_renewal_year)", example = "1")
|
||||||
|
private String effectiveYear;
|
||||||
|
|
||||||
@Schema(description = "备注", example = "随便")
|
@Schema(description = "备注", example = "随便")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ public class RenewalProductRespVO {
|
|||||||
@DictFormat("car_renewal_product_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
@DictFormat("car_renewal_product_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||||
private String productType;
|
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 = "随便")
|
@Schema(description = "备注", example = "随便")
|
||||||
@ExcelProperty("备注")
|
@ExcelProperty("备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public class RenewalProductSaveReqVO {
|
|||||||
@NotEmpty(message = "产品类别(car_renewal_product_type:00 无忧,01 延保)不能为空")
|
@NotEmpty(message = "产品类别(car_renewal_product_type:00 无忧,01 延保)不能为空")
|
||||||
private String productType;
|
private String productType;
|
||||||
|
|
||||||
|
@Schema(description = "生效年限(car_renewal_year)", example = "1")
|
||||||
|
private String effectiveYear;
|
||||||
|
|
||||||
@Schema(description = "备注", example = "随便")
|
@Schema(description = "备注", example = "随便")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@ public class RenewalProductDO extends BaseDO {
|
|||||||
* 枚举 {@link TODO car_renewal_product_type 对应的类}
|
* 枚举 {@link TODO car_renewal_product_type 对应的类}
|
||||||
*/
|
*/
|
||||||
private String productType;
|
private String productType;
|
||||||
|
/**
|
||||||
|
* 生效年限(car_renewal_year)
|
||||||
|
*/
|
||||||
|
private String effectiveYear;
|
||||||
/**
|
/**
|
||||||
* 备注
|
* 备注
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package cn.iocoder.yudao.module.car.service.contract;
|
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.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.api.file.FileApi;
|
||||||
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
@@ -38,6 +40,9 @@ public class ContractServiceImpl implements ContractService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RenewalProductService renewalProductService;
|
||||||
|
|
||||||
private static final String CONTRACT_TEMPLATE = "contract/renewal-contract";
|
private static final String CONTRACT_TEMPLATE = "contract/renewal-contract";
|
||||||
private static final String SEAL_IMAGE_PATH = "static/seal.png";
|
private static final String SEAL_IMAGE_PATH = "static/seal.png";
|
||||||
@@ -160,9 +165,43 @@ public class ContractServiceImpl implements ContractService {
|
|||||||
String certificateNo = generateCertificateNo(renewalOrder);
|
String certificateNo = generateCertificateNo(renewalOrder);
|
||||||
context.setVariable("certificateNo", certificateNo);
|
context.setVariable("certificateNo", certificateNo);
|
||||||
|
|
||||||
// 服务期限
|
// 服务期限:根据产品生效年限计算
|
||||||
LocalDate startDate = LocalDate.now();
|
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("serviceStartDate", startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||||
context.setVariable("serviceEndDate", endDate.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("serviceStartDateText", startDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
|
||||||
context.setVariable("serviceEndDateText", endDate.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)
|
// 签章图片路径(转换为 base64)
|
||||||
// 注意:openhtmltopdf 可能不支持 WebP 格式,如果出错会跳过签章显示
|
// 注意:openhtmltopdf 可能不支持 WebP 格式,如果出错会跳过签章显示
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,4 +68,27 @@ public interface RenewalOrderService {
|
|||||||
*/
|
*/
|
||||||
String generateContractHtml(Long id);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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.framework.common.util.object.BeanUtils;
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.car.dal.mysql.renewalorder.RenewalOrderMapper;
|
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.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.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.*;
|
import static cn.iocoder.yudao.module.car.enums.ErrorCodeConstants.*;
|
||||||
@@ -35,6 +41,12 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ContractService contractService;
|
private ContractService contractService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OnlineSignRedisDAO onlineSignRedisDAO;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private FileApi fileApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long createRenewalOrder(RenewalOrderSaveReqVO createReqVO) {
|
public Long createRenewalOrder(RenewalOrderSaveReqVO createReqVO) {
|
||||||
// 插入
|
// 插入
|
||||||
@@ -107,4 +119,51 @@ public class RenewalOrderServiceImpl implements RenewalOrderService {
|
|||||||
}
|
}
|
||||||
return contractService.generateContractHtml(order);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>途安出行+保障服务凭证</title>
|
<title>途安出行保障服务凭证</title>
|
||||||
<style>
|
<style>
|
||||||
/* ======================
|
/* ======================
|
||||||
* 合同标准排版(OpenHTMLToPDF)
|
* 合同标准排版(OpenHTMLToPDF)
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<div class="wm">途安伴旅</div>
|
<div class="wm">途安伴旅</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<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="meta">
|
||||||
<div class="row"><span class="label">凭证编号:</span><span th:text="${certificateNo}">TABLA20260101001</span></div>
|
<div class="row"><span class="label">凭证编号:</span><span th:text="${certificateNo}">TABLA20260101001</span></div>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">合同条款:</div>
|
<div class="title">合同条款:</div>
|
||||||
<p>
|
<p>
|
||||||
途安出行+保障服务产品为龙岩途安伴旅汽车服务有限公司(以下简称“本公司”)和保险公司合作的一款保障类服务产品,途安出行+保障服务产品已向保险公司投递相关保险。
|
途安出行保障服务产品为龙岩途安伴旅汽车服务有限公司(以下简称“本公司”)和保险公司合作的一款保障类服务产品,途安出行+保障服务产品已向保险公司投递相关保险。
|
||||||
服务合同中双方约定的服务期限、承保范围、保障责任、除外责任、申请资料等内容如下:
|
服务合同中双方约定的服务期限、承保范围、保障责任、除外责任、申请资料等内容如下:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">一、服务期限:</div>
|
<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>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -231,12 +231,11 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">四、补贴标准:</div>
|
<div class="title">四、补贴标准:</div>
|
||||||
<p>
|
<p style="text-indent: 0em;">
|
||||||
权益一:标的车发票金额×10%≤定损金额<标的车发票金额×30%的,补贴金额=定损金额×10%的维修款补贴。服务期限:2026年01月16日至2029年01月16日。<br/>
|
权益一:标的车发票金额x10%≤定损金额<标的车发票金额x30%的,补贴金额=定损金额x10%的维修款补贴。服务期限:<span th:text="${benefit12StartDateText}">2026年01月16日</span>至<span th:text="${benefit12EndDateText}">2029年01月16日</span>。<br/>
|
||||||
权益二:定损金额≥标的车发票金额×30%,且非全损及推定全损时,如车主选择换新车,车主需签署《机动车置换服务合同》让渡原车及商业车险所有权并配合完成标的车辆过户工作,补贴金额=标的车发票金额+车辆购置税+车辆登记费。服务期限:2026年01月16日至2026年01月16日。<br/>
|
权益二:定损金额≥标的车发票金额×30%,且非全损及推定全损时,如车主选择换新车,车主需签署《机动车置换服务合同》让渡原车及商业车险所有权并配合完成标的车辆过户工作,补贴金额=标的车发票金额+车辆购置税+车辆登记费。服务期限:<span th:text="${benefit12StartDateText}">2026年01月16日</span>至<span th:text="${benefit12EndDateText}">2029年01月16日</span>。<br/>
|
||||||
权益三:标的车辆因意外事故导致全损的,补偿金额=置换新车产生的差价费用+车辆登记费。标的车辆因意外事故导致推定全损,补偿金额=车辆折旧金额+车辆登记费。具体补偿金额按以下补贴标准执行。服务期限:2026年01月04日至2029年01月16日。<br/>
|
权益三:标的车辆因双方事故造成的全损,且车主回店置换同品牌新车,补偿金额=置换新车产生的差价费用+车辆登记费;标的车辆因双方事故造成推定全损的,且车主回店置换同品牌新车,补偿金额=车辆折旧金额+车辆登记费。服务期限:<span th:text="${benefit3StartDateText}">xxxx年xx月xx日</span>至<span th:text="${benefit3EndDateText}">xxxx年xx月xx日</span>。(补偿总金额根据此项服务合同生效日开始计算,服务期限前三年内以新车或旧车购置价40%为限,服务期限后两年内以新车或旧车购置价30%为限,均以其低者为准。)具体补偿金额按以下补贴标准执行。说明:a. 服务期内标的车发票金额×10%≤定损金额<标的车发票金额×30%的,每年最多享受2次赔偿且限额5000元/次。(当触发定损金额≥标的车发票金额×30%及全损,补贴责任结束后服务终止);
|
||||||
说明:a. 服务期内标的车发票金额×10%≤定损金额<标的车发票金额×30%的,每年最多享受2次赔偿且限额5000元/次。(当触发定损金额≥标的车发票金额×30%及全损,补贴责任结束后服务终止);b. 置换新车产生的差价费用=标的车发票金额-车损险赔付金额;车辆折旧金额=标的车发票金额×车辆自购买本服务产品之日起计算的已使用月数×0.82%(月折旧率),不足一个月不纳入计算;c. 车辆登记费包括新车注册登记过程中发生的关税、车船税、验车费、上牌费(1000元为限)等政府部门收费。购置税以车辆发票显示的购置税金额或标的车发票金额×购置税率为准;车辆购置税金额及额外费用=车辆购置发票金额×8.55%(上限),触发全损及推定全损情况下上牌费、购置税不超过购车发票价(二手车车辆商业险车损定价)的7%;d. 标的车发票金额/购置税金额与出险时重新购置同款车型新车发票价金额/购置税金额不一致的,以两者取低者为准进行赔付(因同款车型停售或车主主动要求置换其他车型时,同样适用本原则);e. 本服务凭证内所有权益触发赔付时,每次赔付的绝对免赔额为300元/次;
|
(总3页)b. 置换新车产生的差价费用=标的车发票金额-车损险赔付金额;车辆折旧金额=标的车发票金额×车辆自购买本服务产品之日起计算的已使用月数×0.82%(月折旧率),不足一个月不纳入计算; c. 车辆登记费包括新车注册登记过程中发生的关税、车船税、验车费、上牌费(1000元为限)等政府部门收费。购置税以车辆发票显示的购置税金额或标的车发票金额×购置税率为准; 车辆购置税金额及额外费用=车辆购置发票金额×8.55%(上限),触发全损及推定全损情况下上牌费、购置税不超过购车发票价(二手车车辆商业险车损定价)的7%;d. 标的车发票金额/购置税金额与出险时重新购置同款车型新车发票价金额/购置税金额不一致的,以两者取低者为准进行赔付(因同款车型停售或车主主动要求置换其他车型时,同样适用本原则);e.本服务凭证内所有权益触发赔付时,每次赔付的绝对免赔额为300元/次。其他备注:权益一 、权益二仅限购买服务的1-3年期限内有效,权益三依照其限定条件在整个服务周期内均有效,针对同一次事故,或者多次事故同一次进场,都仅可选择一种保障服务且仅限一次赔付,当触发权益二、权益三任何一项换新车服务完成后,此项产品内的所有赔偿服务条款均已结束,定损金额以保险公司车损险维修定损金额为准。
|
||||||
备注:权益一、权益二、权益三仅限购买服务的服务期限内有效。针对同一次事故,或者多次事故同一次进场,都仅可选择一种保障服务且仅限一次赔付,当触发权益二或权益三换新车服务完成后,此项产品内的所有赔偿服务条款均已结束,所有定损金额以保险公司车损险维修定损金额为准。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -276,7 +275,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">八、特别声明:</div>
|
<div class="title">八、特别声明:</div>
|
||||||
<p>
|
<p>
|
||||||
特别声明:车主应当在24小时内向商业险保险公司及交管部门报案,同时致电龙岩途安伴旅汽车服务有限公司全国统一客服热线:18039881137 进行备案,并通过龙岩途安伴旅汽车服务有限公司向保险公司索赔。服务保障正式生效日期:在付款后即T+12日(T为此项服务产品的付款购买日)为车辆使用观察期,此期限内发生的事故将不产生赔付责任,后续为此项服务保障权益的正式生效日期,付款行为以途安伴旅书面确认收到款项之日为准。如系分期客户,若其未按合同/微信通知以及其他告知的约定,按时支付分期款项,经催告后在合理期限内仍未支付的,合同终止,且本公司在款项未结清期限内有权利拒绝赔偿和退还已支付款项。关于退费:凭证生成日起,3天(含)扣除300元工本费无息退还剩余保费,3天以上不支持退款;已发生补贴责任的不可退费。
|
车主应当在24小时内向商业险保险公司及交管部门报案,同时致电龙岩途安伴旅汽车服务有限公司全国统一客服热线:18039881137 进行备案,并通过龙岩途安伴旅汽车服务有限公司向保险公司索赔。服务保障正式生效日(总3页)期:在付款后即T+12日(T为此项服务产品的付款购买日)为车辆使用观察期,此期限内发生的事故将不产生赔付责任,后续为此项服务保障权益的正式生效日期,付款行为以途安伴旅书面确认收到款项之日为准。如系分期客户,若其未按相关告知的约定,按时支付分期款项,经催告后在合理期限内仍未支付的,合同终止,且本公司在款项未结清期限内有权利拒绝赔偿和退还已支付款项 关于退费:凭证生成日起,3天(含)扣除500元工本费无息退还剩余保费,3天以上不支持退款;已发生补贴责任的不可退费。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 末尾签章:左 客户签名+签名(左右排布、剧左),右 服务商+签章(剧右) -->
|
<!-- 末尾签章:左 客户签名+签名(左右排布、剧左),右 服务商+签章(剧右) -->
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -54,17 +54,9 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
# MySQL Connector/J 8.X 连接的示例
|
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 连接的示例
|
||||||
# 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
|
|
||||||
username: root
|
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 连接的示例
|
#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
|
#username: root
|
||||||
@@ -86,9 +78,9 @@ spring:
|
|||||||
|
|
||||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||||
redis:
|
redis:
|
||||||
host: 123.57.150.179 # 地址
|
host: 47.98.192.5 # 地址
|
||||||
port: 6379 # 端口
|
port: 6379 # 端口
|
||||||
database: 0 # 数据库索引
|
database: 1 # 数据库索引
|
||||||
password: redis_pd2R8B # 密码,建议生产环境开启
|
password: redis_pd2R8B # 密码,建议生产环境开启
|
||||||
|
|
||||||
--- #################### 定时任务相关配置 ####################
|
--- #################### 定时任务相关配置 ####################
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ yudao:
|
|||||||
security:
|
security:
|
||||||
permit-all_urls:
|
permit-all_urls:
|
||||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
|
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
|
||||||
|
- /admin-api/car/sign/** # 线上签名页面及接口,无需登录
|
||||||
websocket:
|
websocket:
|
||||||
enable: true # websocket的开关
|
enable: true # websocket的开关
|
||||||
path: /infra/ws # 路径
|
path: /infra/ws # 路径
|
||||||
@@ -246,6 +247,7 @@ yudao:
|
|||||||
- /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
|
- /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
|
||||||
- /jmreport/* # 积木报表,无法携带租户编号
|
- /jmreport/* # 积木报表,无法携带租户编号
|
||||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
|
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
|
||||||
|
- /admin-api/car/sign/** # 线上签名页面及接口,无法携带租户编号
|
||||||
ignore-tables:
|
ignore-tables:
|
||||||
- system_tenant
|
- system_tenant
|
||||||
- system_tenant_package
|
- system_tenant_package
|
||||||
|
|||||||
Reference in New Issue
Block a user