增加拉卡拉电子合同html页面

This commit is contained in:
Jack 2025-09-15 22:37:43 +08:00
parent 5da423e50b
commit b8627633fb
7 changed files with 495 additions and 17 deletions

View File

@ -6,11 +6,18 @@ import com.suisung.mall.common.api.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.sql.SQLException;
import java.util.stream.Collectors;
/**
* 全局异常处理器
@ -25,22 +32,62 @@ import java.sql.SQLException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final String UNKNOWN_LOCATION = "未知位置";
private static final String DB_ERROR_MSG = "数据库操作失败,请稍后重试";
private static final String LOG_FORMAT = "业务异常 || URI={} || Method={} || Error={} || Location={}";
/**
* 获取异常发生位置信息
*
* @param stackTrace 异常堆栈
* @return 格式化的位置信息(类名.方法名 : 行号)
* 处理参数校验异常
*/
private String getExceptionLocation(StackTraceElement[] stackTrace) {
if (stackTrace == null || stackTrace.length == 0) {
return UNKNOWN_LOCATION;
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public CommonResult handleValidationException(Exception e, HttpServletRequest req) {
String errorMessage;
if (e instanceof MethodArgumentNotValidException) {
errorMessage = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
} else {
errorMessage = ((BindException) e).getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
}
StackTraceElement first = stackTrace[0];
return String.format("%s.%s:%d", first.getClassName(), first.getMethodName(), first.getLineNumber());
logError(req, "参数校验失败", e);
return CommonResult.validateFailed(errorMessage);
}
/**
* 处理约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public CommonResult handleConstraintViolation(ConstraintViolationException e, HttpServletRequest req) {
String errorMessage = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
logError(req, "约束违反异常", e);
return CommonResult.validateFailed(errorMessage);
}
/**
* 处理参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public CommonResult handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException e, HttpServletRequest req) {
String errorMessage = String.format("参数%s类型不匹配需要%s类型但接收到%s",
e.getName(), e.getRequiredType().getSimpleName(), e.getValue());
logError(req, "参数类型不匹配", e);
return CommonResult.validateFailed(errorMessage);
}
/**
* 处理数字格式异常和其他参数异常
*/
@ExceptionHandler({NumberFormatException.class, IllegalArgumentException.class})
public CommonResult handleParameterException(Exception e, HttpServletRequest req) {
String errorMessage = e.getMessage();
logError(req, "参数异常", e);
return CommonResult.validateFailed(errorMessage);
}
/**
@ -48,8 +95,7 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler({SQLException.class, DataAccessException.class, Exception.class})
public CommonResult handleSystemException(HttpServletRequest req, Exception e) {
String location = getExceptionLocation(e.getStackTrace());
logger.error(LOG_FORMAT, req.getRequestURI(), req.getMethod(), e.getMessage(), location, e);
logError(req, e.getMessage(), e);
if (e instanceof SQLException || e instanceof DataAccessException) {
return CommonResult.failed(DB_ERROR_MSG);
@ -62,12 +108,22 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(ApiException.class)
public CommonResult handleApiException(HttpServletRequest req, ApiException e) {
String location = getExceptionLocation(e.getStackTrace());
logger.error(LOG_FORMAT,
req.getRequestURI(), req.getMethod(), e.getErrorCode(), location, e);
logError(req, e.getErrorCode() != null ? e.getErrorCode().getMessage() : e.getMessage(), e);
return StrUtil.isNotBlank(e.getMessage())
? CommonResult.failed(e.getMessage())
: CommonResult.failed(ResultCode.FAILED);
}
/**
* 记录错误日志
*/
private void logError(HttpServletRequest req, String message, Exception e) {
StackTraceElement[] stackTrace = e.getStackTrace();
String location = (stackTrace == null || stackTrace.length == 0)
? "未知位置"
: String.format("%s.%s:%d", stackTrace[0].getClassName(), stackTrace[0].getMethodName(), stackTrace[0].getLineNumber());
logger.error("业务异常 || URI={} || Method={} || Error={} || Location={}",
req.getRequestURI(), req.getMethod(), message, location, e);
}
}

View File

@ -90,6 +90,7 @@ secure:
- "/esProduct/**"
- "/admin/oss/upload/**"
- "/admin/shop/wxqrcode/common/wxurlscheme"
- "/mobile/shop/lakala/sign/ec/**"
- "/mobile/**/**/test/case"
- "/**/**/testcase"
universal:

View File

@ -273,6 +273,11 @@
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>

View File

@ -13,6 +13,7 @@ import cn.hutool.json.JSONUtil;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.service.impl.BaseControllerImpl;
import com.suisung.mall.shop.lakala.service.LakalaApiService;
import com.suisung.mall.shop.lakala.service.LklLedgerEcService;
import com.suisung.mall.shop.library.service.LibraryProductService;
import com.suisung.mall.shop.message.service.MqMessageService;
import com.suisung.mall.shop.message.service.PushMessageService;
@ -28,6 +29,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@ -71,6 +73,9 @@ public class LakalaController extends BaseControllerImpl {
@Resource
private MqMessageService mqMessageService;
@Resource
private LklLedgerEcService lklLedgerEcService;
@ApiOperation(value = "测试案例", notes = "测试案例")
@RequestMapping(value = "/testcase", method = RequestMethod.POST)
public Object testcase(@RequestBody JSONObject paramsJSON) {
@ -154,6 +159,26 @@ public class LakalaController extends BaseControllerImpl {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(resp);
}
@ApiOperation(value = "跳转到拉卡拉签署合同链接", notes = "跳转到拉卡拉签署合同链接")
@RequestMapping(value = "/sign/ec/{code}", method = RequestMethod.GET)
public ModelAndView jumpToSignLklEcLink(@PathVariable Long code) {
// // 根据 code 值决定重定向到哪个 URL
// String resultUrl = lklLedgerEcService.getLklEcResultUrl(code);
// if (StrUtil.isBlank(resultUrl)) {
// return new RedirectView("https://mall.gpxscs.cn/mchapp");
// }
// return new RedirectView(resultUrl);
// 根据 code 值获取结果 URL
String resultUrl = lklLedgerEcService.getLklEcResultUrl(code);
ModelAndView modelAndView = new ModelAndView("sign_lkl_ec");
modelAndView.addObject("resultUrl", resultUrl);
// 返回模板名称渲染 sign_lkl_ec.html 页面
return modelAndView;
}
@ApiOperation(value = "商户分账业务开通申请", notes = "商户分账业务开通申请")
@RequestMapping(value = "/ledger/applyLedgerMer", method = RequestMethod.POST)
public CommonResult ledgerApplyLedgerMer(@RequestBody JSONObject paramsJSON) {

View File

@ -50,4 +50,12 @@ public interface LklLedgerEcService extends IBaseService<LklLedgerEc> {
LklLedgerEc getByMchId(Long mchId, String ecStatus, Integer status);
/**
* 获取拉卡拉商户签署合同页面URL
*
* @param mchId
* @return
*/
String getLklEcResultUrl(Long mchId);
}

View File

@ -135,4 +135,19 @@ public class LklLedgerEcServiceImpl extends BaseServiceImpl<LklLedgerEcMapper, L
return findOne(queryWrapper);
}
/**
* 获取拉卡拉商户签署合同页面URL
*
* @param mchId
* @return
*/
@Override
public String getLklEcResultUrl(Long mchId) {
LklLedgerEc record = getByMchId(mchId, "", null);
if (record != null) {
return record.getResult_url();
}
return "";
}
}

View File

@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>小发同城电子合同签署</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
line-height: 1.6;
color: #333;
font-size: 16px;
}
.container {
max-width: 100%;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #4CAF50, #2E7D32);
color: white;
padding: 30px 20px 20px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header h1 {
margin: 0;
font-size: 22px;
font-weight: 500;
letter-spacing: 1px;
}
.content {
flex: 1;
padding: 30px 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.success-icon {
width: 80px;
height: 80px;
background-color: #e8f5e9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
}
.success-icon span {
font-size: 40px;
color: #4CAF50;
}
.message {
text-align: center;
margin-bottom: 30px;
width: 100%;
}
.message p {
margin-bottom: 20px;
font-size: 16px;
color: #555;
}
.message p:first-child {
font-size: 18px;
color: #333;
font-weight: 500;
}
.link-container {
background-color: #f0f9ff;
border: 1px dashed #2196F3;
border-radius: 12px;
padding: 20px 15px;
margin: 25px 0;
width: 100%;
text-align: center;
}
.link-title {
font-weight: 500;
color: #1976D2;
margin-bottom: 12px;
font-size: 16px;
}
.link {
color: #1976D2;
word-break: break-all;
font-size: 15px;
font-family: monospace;
padding: 10px;
background-color: #e3f2fd;
border-radius: 8px;
margin-top: 5px;
}
.app-option {
background-color: #fff3e0;
border: 1px solid #ffcc80;
border-radius: 12px;
padding: 20px 15px;
margin: 20px 0;
text-align: center;
}
.app-option p {
margin: 0;
font-size: 16px;
color: #e65100;
}
.app-icon {
font-size: 36px;
margin-bottom: 10px;
}
.warning {
background-color: #fff8e1;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 25px 0;
text-align: left;
border-radius: 0 6px 6px 0;
font-size: 15px;
}
.warning strong {
color: #e65100;
}
.footer {
background-color: #fafafa;
padding: 20px 15px;
text-align: center;
color: #666;
font-size: 14px;
border-top: 1px solid #eee;
}
.contact {
color: #1976D2;
text-decoration: none;
font-weight: 500;
}
.contact:hover, .contact:active {
text-decoration: underline;
}
.btn {
display: block;
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
text-align: center;
border: none;
border-radius: 10px;
font-size: 17px;
font-weight: 500;
margin: 20px 0;
cursor: pointer;
box-shadow: 0 4px 6px rgba(33, 150, 243, 0.2);
}
.btn:active {
transform: translateY(1px);
box-shadow: 0 2px 3px rgba(33, 150, 243, 0.2);
}
.copy-btn {
background: #e3f2fd;
color: #1976D2;
margin-top: 10px;
}
/* 移动端优化 */
@media (max-width: 480px) {
.header {
padding: 25px 15px 15px;
}
.header h1 {
font-size: 20px;
}
.content {
padding: 20px 15px;
}
.success-icon {
width: 70px;
height: 70px;
}
.success-icon span {
font-size: 35px;
}
.message p {
font-size: 15px;
}
.message p:first-child {
font-size: 17px;
}
.link-container, .app-option {
padding: 15px 12px;
}
.link {
font-size: 14px;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
body {
font-size: 15px;
}
.header h1 {
font-size: 18px;
}
.btn {
padding: 14px;
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>小发同城电子合同签署</h1>
</div>
<div class="content">
<div class="success-icon">
<span></span>
</div>
<div class="message">
<p>恭喜!您的开店入驻申请已审核通过</p>
<p>请在24小时内完成电子合同签署</p>
<div class="link-container">
<div class="link-title">合作方签署链接24小时内有效</div>
<div class="link" id="signLink" th:text="${resultUrl}">${resultUrl}</div>
<button class="btn copy-btn" onclick="copyLink()">复制链接</button>
</div>
<!-- <div class="app-option">-->
<!-- <div class="app-icon">📱</div>-->
<!-- <p>或前往<strong>小发商家版APP</strong>完成签署</p>-->
<!-- </div>-->
<button class="btn" onclick="openLink()">立即签署合同</button>
<div class="warning">
<strong>注意:</strong>链接有效期为24小时逾期需重新提交申请。
</div>
</div>
</div>
<div class="footer">
<p>如有疑问请联系客服:<a class="contact" href="tel:17777525395">17777525395</a></p>
<p>感谢您对小发同城的支持!</p>
</div>
</div>
<script th:inline="javascript">
// 获取后端传递的resultUrl变量
var resultUrl = '${resultUrl}';
// 复制链接功能
function copyLink() {
var linkText = document.getElementById('signLink').textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(linkText).then(() => {
alert('链接已复制到剪贴板');
}).catch(err => {
console.error('复制失败:', err);
fallbackCopyTextToClipboard(linkText);
});
} else {
fallbackCopyTextToClipboard(linkText);
}
}
// 兼容性复制方法
function fallbackCopyTextToClipboard(text) {
var textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
var successful = document.execCommand('copy');
if (successful) {
alert('链接已复制到剪贴板');
} else {
prompt("请手动复制以下链接:", text);
}
} catch (err) {
prompt("请手动复制以下链接:", text);
}
document.body.removeChild(textArea);
}
// 打开链接功能
function openLink() {
if (resultUrl) {
// 如果是完整URL则直接跳转
if (resultUrl.startsWith('http') || resultUrl.startsWith('https')) {
window.location.href = resultUrl;
} else {
// 否则添加协议前缀
window.location.href = 'https://' + resultUrl;
}
} else {
alert('链接无效,请联系客服');
}
}
// 检测是否在微信中打开
var ua = navigator.userAgent.toLowerCase();
if (ua.includes('micromessenger')) {
// 在微信中显示提示
var btn = document.querySelector('.btn');
if (btn) {
btn.textContent = '点击右上角菜单选择在浏览器中打开';
}
}
// 防止页面被嵌入到iframe中
if (window.self !== window.top) {
window.top.location = window.location;
}
</script>
</body>
</html>