diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java
new file mode 100644
index 000000000..0e0add244
--- /dev/null
+++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java
@@ -0,0 +1,57 @@
+package com.bsd.pjcloud.common.core.util;
+
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 弱口令校验:禁止密码与手机号/用户名相同等低强度规则。
+ */
+public final class PasswordWeakCheckUtil {
+
+ private PasswordWeakCheckUtil() {
+ }
+
+ /**
+ * @param rawPassword 明文密码
+ * @param phone 手机号(可空)
+ * @param username 用户名(可空)
+ * @return 不通过时的中文原因;通过返回 null
+ */
+ public static String validate(String rawPassword, String phone, String username) {
+ if (StrUtil.isBlank(rawPassword)) {
+ return "密码不能为空";
+ }
+ String pwd = StrUtil.trim(rawPassword);
+ String phoneTrim = StrUtil.trim(phone);
+ String userTrim = StrUtil.trim(username);
+ if (StrUtil.isNotBlank(phoneTrim) && pwd.equals(phoneTrim)) {
+ return "密码不能与手机号相同";
+ }
+ if (StrUtil.isNotBlank(userTrim) && pwd.equalsIgnoreCase(userTrim)) {
+ return "密码不能与用户名相同";
+ }
+ if (pwd.length() < 8) {
+ return "密码长度不能少于8位";
+ }
+ boolean hasLetter = false;
+ boolean hasDigit = false;
+ for (int i = 0; i < pwd.length(); i++) {
+ char c = pwd.charAt(i);
+ if (Character.isLetter(c)) {
+ hasLetter = true;
+ } else if (Character.isDigit(c)) {
+ hasDigit = true;
+ }
+ }
+ if (!hasLetter || !hasDigit) {
+ return "密码必须同时包含字母和数字";
+ }
+ return null;
+ }
+
+ public static void assertStrong(String rawPassword, String phone, String username) {
+ String err = validate(rawPassword, phone, username);
+ if (err != null) {
+ throw new IllegalArgumentException(err);
+ }
+ }
+}
diff --git a/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties b/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties
index 0699ff526..8e19e265d 100644
--- a/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties
+++ b/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties
@@ -1,6 +1,6 @@
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
-sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
+sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u89C1\u7CFB\u7EDF\u53C2\u6570USER_DEFAULT_PASSWORD
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
diff --git a/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java
index 564abb52f..8c4d7aca5 100644
--- a/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java
+++ b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java
@@ -35,76 +35,107 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-06-24
*
- * 解决Mybatis Plus Order By SQL注入问题
+ * 解决Mybatis Plus Order By SQL注入问题:ascs/descs 仅允许简单列名白名单,禁止表达式/函数注入。
*/
@Slf4j
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
+ /**
+ * 排序字段白名单:仅允许标识符(可选表别名.列名),禁止空格、括号、逗号、注释等。
+ * 例:create_time、user_id、u.createTime
+ */
+ private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile(
+ "^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$");
- /**
- * 判断Controller是否包含page 参数
- *
- * @param parameter 参数
- * @return 是否过滤
- */
- @Override
- public boolean supportsParameter(MethodParameter parameter) {
- return parameter.getParameterType().equals(Page.class);
- }
+ /** 单次请求最多允许的排序列数量,防止恶意堆叠。 */
+ private static final int MAX_ORDER_COLUMNS = 5;
- /**
- * @param parameter 入参集合
- * @param mavContainer model 和 view
- * @param webRequest web相关
- * @param binderFactory 入参解析
- * @return 检查后新的page对象
- *
- * page 只支持查询 GET .如需解析POST获取请求报文体处理
- */
- @Override
- public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
- NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
+ /**
+ * 判断Controller是否包含page 参数
+ *
+ * @param parameter 参数
+ * @return 是否过滤
+ */
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return parameter.getParameterType().equals(Page.class);
+ }
- HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+ /**
+ * @param parameter 入参集合
+ * @param mavContainer model 和 view
+ * @param webRequest web相关
+ * @param binderFactory 入参解析
+ * @return 检查后新的page对象
+ *
+ * page 只支持查询 GET .如需解析POST获取请求报文体处理
+ */
+ @Override
+ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
- String ascs = request.getParameter("ascs");
- String descs = request.getParameter("descs");
+ HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
- String current = request.getParameter("current");
- String size = request.getParameter("size");
+ String ascs = request.getParameter("ascs");
+ String descs = request.getParameter("descs");
- Page page = new Page();
- if (StrUtil.isNotBlank(current)) {
- // 如果current page 小于零 视为不合法数据
- if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
- throw new CheckedException("current page error");
- }
- page.setCurrent(Long.parseLong(current));
- }
+ String current = request.getParameter("current");
+ String size = request.getParameter("size");
- if (StrUtil.isNotBlank(size)) {
- page.setSize(Long.parseLong(size));
- }
+ Page page = new Page();
+ if (StrUtil.isNotBlank(current)) {
+ // 如果current page 小于零 视为不合法数据
+ if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
+ throw new CheckedException("current page error");
+ }
+ page.setCurrent(Long.parseLong(current));
+ }
- List orderItemList = new ArrayList<>();
- Optional.ofNullable(ascs)
- .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
- .filter(asc -> !SqlInjectionUtils.check(asc))
- .map(OrderItem::asc)
- .collect(Collectors.toList())));
- Optional.ofNullable(descs)
- .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
- .filter(desc -> !SqlInjectionUtils.check(desc))
- .map(OrderItem::desc)
- .collect(Collectors.toList())));
- page.addOrder(orderItemList);
+ if (StrUtil.isNotBlank(size)) {
+ page.setSize(Long.parseLong(size));
+ }
+
+ List orderItemList = new ArrayList<>();
+ Optional.ofNullable(ascs)
+ .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
+ .map(String::trim)
+ .filter(StrUtil::isNotBlank)
+ .map(col -> toSafeOrderColumn(col, true))
+ .collect(Collectors.toList())));
+ Optional.ofNullable(descs)
+ .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
+ .map(String::trim)
+ .filter(StrUtil::isNotBlank)
+ .map(col -> toSafeOrderColumn(col, false))
+ .collect(Collectors.toList())));
+ if (orderItemList.size() > MAX_ORDER_COLUMNS) {
+ throw new CheckedException("排序字段数量超限");
+ }
+ page.addOrder(orderItemList);
+
+ return page;
+ }
+
+ /**
+ * 将前端排序字段转为安全的 OrderItem:必须通过标识符白名单,且通过 SqlInjectionUtils 黑名单兜底。
+ */
+ private static OrderItem toSafeOrderColumn(String column, boolean asc) {
+ if (!SAFE_ORDER_COLUMN.matcher(column).matches()) {
+ log.warn("拒绝非法排序字段: {}", column);
+ throw new CheckedException("排序字段不合法");
+ }
+ if (SqlInjectionUtils.check(column)) {
+ log.warn("排序字段命中注入检测: {}", column);
+ throw new CheckedException("排序字段不合法");
+ }
+ return asc ? OrderItem.asc(column) : OrderItem.desc(column);
+ }
- return page;
- }
}
diff --git a/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java b/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java
index 7e5036f6d..5856038be 100644
--- a/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java
+++ b/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java
@@ -18,8 +18,10 @@
package com.bsd.pjcloud.common.sentinel.handle;
import com.alibaba.csp.sentinel.Tracer;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.bsd.pjcloud.common.core.exception.CheckedException;
+import com.bsd.pjcloud.common.core.exception.ServiceException;
import com.bsd.pjcloud.common.core.util.R;
+import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
@@ -40,111 +42,145 @@ import java.util.List;
@RestControllerAdvice
public class GlobalBizExceptionHandler {
- private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectMapper objectMapper = new ObjectMapper();
- /**
- * 全局异常.
- *
- * @param e the e
- * @return R
- */
- @ExceptionHandler(Exception.class)
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleGlobalException(Exception e) {
+ /**
+ * 业务可控异常:允许把明确的业务文案返回前端。
+ */
+ @ExceptionHandler({CheckedException.class, ServiceException.class, IllegalArgumentException.class})
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleBizException(RuntimeException e) {
+ log.warn("业务异常 ex={}", e.getMessage());
+ return R.failed(e.getMessage());
+ }
- log.error("全局异常信息 ex={}", e.getMessage(), e);
+ /**
+ * SQLException:禁止把完整 SQL / 表结构回显给前端。
+ */
+ @ExceptionHandler({SQLException.class})
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleSQLException(SQLException exception) {
+ log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
+ return R.failed("数据库调用异常,请联系管理员处理");
+ }
- // 业务异常交由 sentinel 记录
- Tracer.trace(e);
- return R.failed(e.getLocalizedMessage());
- }
+ /**
+ * 全局异常:默认不回显底层异常原文(MyBatis 常把 SQL、mapper 路径塞进 message)。
+ *
+ * @param e the e
+ * @return R
+ */
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleGlobalException(Exception e) {
+ log.error("全局异常信息 ex={}", e.getMessage(), e);
+ Tracer.trace(e);
+ if (isDataAccessRelated(e)) {
+ return R.failed("数据库调用异常,请联系管理员处理");
+ }
+ return R.failed("系统繁忙,请稍后重试");
+ }
- /**
- * SQLException Exception
- *
- * @param exception 数据库调用异常
- * @return R
- */
- @ExceptionHandler({SQLException.class})
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleSQLException(SQLException exception) {
- log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
- return R.failed("数据库调用异常,请联系管理员处理");
- }
+ /**
+ * 识别 Spring DAO / MyBatis / JDBC 相关异常(本模块不直接依赖 mybatis,用类名与特征串判断)。
+ */
+ private static boolean isDataAccessRelated(Throwable e) {
+ Throwable t = e;
+ while (t != null) {
+ if (t instanceof SQLException) {
+ return true;
+ }
+ String name = t.getClass().getName();
+ if (name.startsWith("org.springframework.dao.")
+ || name.startsWith("org.springframework.jdbc.")
+ || name.contains("MyBatis")
+ || name.contains("PersistenceException")
+ || name.contains("BadSqlGrammar")) {
+ return true;
+ }
+ String msg = t.getMessage();
+ if (msg != null && (msg.contains("### Error") || msg.contains("### SQL")
+ || msg.contains("SQL:") || msg.contains("Cause: java.sql")
+ || msg.contains(" bad SQL grammar"))) {
+ return true;
+ }
+ t = t.getCause();
+ }
+ return false;
+ }
- @ExceptionHandler(FeignException.class)
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleGlobalException(FeignException e) {
- log.error("全局异常信息 ex={}", e.getMessage(), e);
+ @ExceptionHandler(FeignException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleGlobalException(FeignException e) {
+ log.error("全局异常信息 ex={}", e.getMessage(), e);
- // 业务异常交由 sentinel 记录
- Tracer.trace(e);
+ // 业务异常交由 sentinel 记录
+ Tracer.trace(e);
- if (e.responseBody().isPresent()) {
- try {
- return objectMapper.readValue(e.responseBody().get().array(), R.class);
- } catch (Exception parseEx) {
- // 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
- log.warn("Feign 错误响应非 JSON,status={}", e.status());
- return R.failed("下游服务异常,请稍后重试");
- }
- }
+ if (e.responseBody().isPresent()) {
+ try {
+ return objectMapper.readValue(e.responseBody().get().array(), R.class);
+ } catch (Exception parseEx) {
+ // 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
+ log.warn("Feign 错误响应非 JSON,status={}", e.status());
+ return R.failed("下游服务异常,请稍后重试");
+ }
+ }
- return R.failed("服务调用失败,请稍后重试");
- }
+ return R.failed("服务调用失败,请稍后重试");
+ }
- /**
- * AccessDeniedException
- *
- * @param e the e
- * @return R
- */
- @ExceptionHandler(AccessDeniedException.class)
- @ResponseStatus(HttpStatus.FORBIDDEN)
- public R handleAccessDeniedException(AccessDeniedException e) {
- log.error("拒绝授权异常信息 ex={}", e.getMessage());
- return R.failed("权限不足,不允许访问");
- }
+ /**
+ * AccessDeniedException
+ *
+ * @param e the e
+ * @return R
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public R handleAccessDeniedException(AccessDeniedException e) {
+ log.error("拒绝授权异常信息 ex={}", e.getMessage());
+ return R.failed("权限不足,不允许访问");
+ }
- /**
- * validation Exception
- *
- * @param exception
- * @return R
- */
- @ExceptionHandler({BindException.class})
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- public R handleBodyValidException(BindException exception) {
- List fieldErrors = exception.getBindingResult().getFieldErrors();
- // 插入log 的逻辑
- return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
- }
+ /**
+ * validation Exception
+ *
+ * @param exception
+ * @return R
+ */
+ @ExceptionHandler({BindException.class})
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleBodyValidException(BindException exception) {
+ List fieldErrors = exception.getBindingResult().getFieldErrors();
+ return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
+ }
- /**
- * 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
- *
- * @return R
- */
- @DeleteMapping("/error")
- @ResponseStatus(HttpStatus.NOT_FOUND)
- public R noHandlerFoundException() {
- return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
- }
+ /**
+ * 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
+ *
+ * @return R
+ */
+ @DeleteMapping("/error")
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public R noHandlerFoundException() {
+ return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
+ }
- /**
- * 保持和低版本请求路径不存在的行为一致
- *
- * [Spring Boot
- * 3.2.0] 404 Not Found behavior #38733
- *
- * @param exception
- * @return R
- */
- @ExceptionHandler({NoResourceFoundException.class})
- @ResponseStatus(HttpStatus.NOT_FOUND)
- public R noResourceFoundException(NoResourceFoundException exception) {
- log.debug("请求路径 404 {}", exception.getMessage());
- return R.failed(exception.getMessage());
- }
+ /**
+ * 保持和低版本请求路径不存在的行为一致
+ *
+ * [Spring Boot
+ * 3.2.0] 404 Not Found behavior #38733
+ *
+ * @param exception
+ * @return R
+ */
+ @ExceptionHandler({NoResourceFoundException.class})
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public R noResourceFoundException(NoResourceFoundException exception) {
+ log.debug("请求路径 404 {}", exception.getMessage());
+ return R.failed(exception.getMessage());
+ }
}
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java
new file mode 100644
index 000000000..eddde9612
--- /dev/null
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java
@@ -0,0 +1,129 @@
+package com.bsd.pjcloud.thirdparty.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.log.annotation.SysLog;
+import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
+import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdPhoneBindService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 浙政钉短信核验手机号绑定:后台查询/改号(绑错时可修正)
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/zzdPhoneBind")
+@Tag(description = "zzdPhoneBind", name = "浙政钉手机号绑定管理")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class ThirdPartyZzdPhoneBindController {
+
+ private final ThirdPartyZzdPhoneBindService thirdPartyZzdPhoneBindService;
+
+ @Operation(summary = "分页查询")
+ @GetMapping("/page")
+ @PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_view')")
+ public R page(@ParameterObject Page page,
+ @RequestParam(value = "name", required = false) String name,
+ @RequestParam(value = "verifiedPhone", required = false) String verifiedPhone,
+ @RequestParam(value = "employeeCode", required = false) String employeeCode) {
+ LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();
+ wrapper.eq(ThirdPartyZzdPhoneBindEntity::getDelFlag, "0");
+ wrapper.like(StringUtils.isNotBlank(verifiedPhone), ThirdPartyZzdPhoneBindEntity::getVerifiedPhone, verifiedPhone);
+ wrapper.like(StringUtils.isNotBlank(employeeCode), ThirdPartyZzdPhoneBindEntity::getEmployeeCode, employeeCode);
+ // 名称在 zzd_profile_json(nickName/username)里,模糊匹配 JSON 文本
+ wrapper.like(StringUtils.isNotBlank(name), ThirdPartyZzdPhoneBindEntity::getZzdProfileJson, name);
+ wrapper.orderByDesc(ThirdPartyZzdPhoneBindEntity::getUpdateTime)
+ .orderByDesc(ThirdPartyZzdPhoneBindEntity::getCreateTime);
+ Page result = thirdPartyZzdPhoneBindService.page(page, wrapper);
+ List