feat(ocr): 集成百度智能云身份证OCR识别服务
- 替换原有失效的身份证OCR服务为百度智能云OCR,支持身份证正面信息识别 - 身份证号、姓名、住址识别成功后回填至订单和用户信息,驱动支付流程状态 - 新增后端配置项管理百度OCR的API密钥,保证密钥仅服务端可见 - 调用失败时仅日志警告,不阻断业务,前端可手动填写身份证号兜底 - 新增数据库字段 id_code、real_name、address 支持身份证识别数据存储 - 更新确认收货接口参数,新增身份证号字段支持前端手动输入 - 更新相关代码逻辑,使用百度OCR接口实现最佳努力模式的身份证识别及回填 - 修改配置文件调整默认激活环境和数据库连接信息,支持开发与生产环境分离
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>com.gxwebsoft</groupId>
|
<groupId>com.gxwebsoft</groupId>
|
||||||
<artifactId>yxw-api</artifactId>
|
<artifactId>yxw-api</artifactId>
|
||||||
<version>1.5.2</version>
|
<version>1.0</version>
|
||||||
|
|
||||||
<name>yxw-api</name>
|
<name>yxw-api</name>
|
||||||
<description>WebSoftApi project for Spring Boot</description>
|
<description>WebSoftApi project for Spring Boot</description>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.gxwebsoft.common.core.utils;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度身份证 OCR 识别结果(仅取前端面所需字段)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BaiduIdCardResult implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 公民身份号码 */
|
||||||
|
private String idCardNo;
|
||||||
|
/** 姓名 */
|
||||||
|
private String name;
|
||||||
|
/** 住址 */
|
||||||
|
private String address;
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.gxwebsoft.common.core.utils;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度智能云「身份证识别」服务(替换原已失效的第三方 OCR)。
|
||||||
|
* 密钥在服务端配置,不暴露给前端;access_token 进程内缓存。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class BaiduOcrService {
|
||||||
|
|
||||||
|
@Value("${baidu.ocr.api-key:}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
@Value("${baidu.ocr.secret-key:}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RestTemplate restTemplate;
|
||||||
|
|
||||||
|
private static final String TOKEN_URL =
|
||||||
|
"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s";
|
||||||
|
private static final String OCR_URL =
|
||||||
|
"https://aip.baidubce.com/rest/2.0/ocr/v1/idcard?access_token=%s";
|
||||||
|
|
||||||
|
private static final Pattern ID_CARD_PATTERN = Pattern.compile("^\\d{17}[\\dXx]$");
|
||||||
|
|
||||||
|
/** 进程内 token 缓存 */
|
||||||
|
private String cachedToken;
|
||||||
|
private long tokenExpireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别身份证正面,提取 身份证号/姓名/住址。
|
||||||
|
*
|
||||||
|
* @param base64Image 图片 base64(不含 data uri 前缀)
|
||||||
|
* @return 识别结果;调用失败/未识别到身份证号时返回 null
|
||||||
|
*/
|
||||||
|
public BaiduIdCardResult recognizeFront(String base64Image) {
|
||||||
|
if (base64Image == null || base64Image.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String token = getToken();
|
||||||
|
if (token == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("id_card_side", "front");
|
||||||
|
body.add("image", base64Image);
|
||||||
|
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
String resp = restTemplate.postForObject(String.format(OCR_URL, token), entity, String.class);
|
||||||
|
JSONObject json = JSONObject.parseObject(resp);
|
||||||
|
if (json == null) {
|
||||||
|
log.warn("百度OCR返回为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (json.containsKey("error_code")) {
|
||||||
|
log.warn("百度OCR错误 error_code={}, error_msg={}", json.getString("error_code"), json.getString("error_msg"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject words = json.getJSONObject("words_result");
|
||||||
|
if (words == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BaiduIdCardResult result = new BaiduIdCardResult();
|
||||||
|
JSONObject idNode = words.getJSONObject("公民身份号码");
|
||||||
|
if (idNode != null && idNode.getString("words") != null) {
|
||||||
|
result.setIdCardNo(idNode.getString("words").trim());
|
||||||
|
}
|
||||||
|
JSONObject nameNode = words.getJSONObject("姓名");
|
||||||
|
if (nameNode != null && nameNode.getString("words") != null) {
|
||||||
|
result.setName(nameNode.getString("words").trim());
|
||||||
|
}
|
||||||
|
JSONObject addrNode = words.getJSONObject("住址");
|
||||||
|
if (addrNode != null && addrNode.getString("words") != null) {
|
||||||
|
result.setAddress(addrNode.getString("words").trim());
|
||||||
|
}
|
||||||
|
// 身份证号不合法时视为未识别
|
||||||
|
if (result.getIdCardNo() == null || !ID_CARD_PATTERN.matcher(result.getIdCardNo()).matches()) {
|
||||||
|
log.warn("百度OCR未识别到合法身份证号 idCardNo={}", result.getIdCardNo());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("百度OCR调用异常", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getToken() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (cachedToken != null && now < tokenExpireTime) {
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
if (apiKey == null || apiKey.isEmpty() || secretKey == null || secretKey.isEmpty()) {
|
||||||
|
log.error("百度OCR未配置 api-key/secret-key,无法获取 token");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String url = String.format(TOKEN_URL, apiKey, secretKey);
|
||||||
|
String resp = restTemplate.getForObject(url, String.class);
|
||||||
|
JSONObject json = JSONObject.parseObject(resp);
|
||||||
|
if (json == null || json.getString("access_token") == null) {
|
||||||
|
log.error("获取百度OCR token失败 resp={}", resp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
cachedToken = json.getString("access_token");
|
||||||
|
long expiresIn = json.getLongValue("expires_in");
|
||||||
|
// 提前 5 分钟过期,避免临界失效
|
||||||
|
tokenExpireTime = now + (expiresIn - 300) * 1000L;
|
||||||
|
return cachedToken;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取百度OCR token异常", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ import com.gxwebsoft.common.core.utils.ImageUtil;
|
|||||||
import com.gxwebsoft.common.core.web.*;
|
import com.gxwebsoft.common.core.web.*;
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import com.gxwebsoft.common.system.service.UserService;
|
import com.gxwebsoft.common.system.service.UserService;
|
||||||
import com.gxwebsoft.oa.service.IdcardService;
|
import com.gxwebsoft.common.core.utils.BaiduIdCardResult;
|
||||||
import com.gxwebsoft.oa.vo.IdcardRespVO;
|
import com.gxwebsoft.common.core.utils.BaiduOcrService;
|
||||||
import com.gxwebsoft.shop.entity.Order;
|
import com.gxwebsoft.shop.entity.Order;
|
||||||
import com.gxwebsoft.shop.entity.OrderPay;
|
import com.gxwebsoft.shop.entity.OrderPay;
|
||||||
import com.gxwebsoft.shop.entity.OrderRefund;
|
import com.gxwebsoft.shop.entity.OrderRefund;
|
||||||
@@ -80,7 +80,7 @@ public class OpenEquipmentController extends BaseController {
|
|||||||
private OrderPayService orderPayService;
|
private OrderPayService orderPayService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IdcardService idcardService;
|
private BaiduOcrService baiduOcrService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private RestTemplate restTemplate;
|
private RestTemplate restTemplate;
|
||||||
@@ -439,20 +439,13 @@ public class OpenEquipmentController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 身份证 OCR 真实性校验,并把识别到的实名信息回填到 order / user。
|
* 身份证 OCR 真实性校验(「尽力而为」模式:OCR 调不通/未识别到身份证号时仅记录日志,不阻断业务)。
|
||||||
|
* 使用百度智能云身份证识别,识别成功时把 身份证号/姓名/住址 回填到 order / user,
|
||||||
|
* 其中 order.idCode 用于驱动前端「确认身份 → 去支付」的状态流转。
|
||||||
* orderSourceData 约定顺序:[0]=身份证正面、[1]=身份证反面、[2]=人车合照、[3]=车子照片、[4]=安装照片
|
* orderSourceData 约定顺序:[0]=身份证正面、[1]=身份证反面、[2]=人车合照、[3]=车子照片、[4]=安装照片
|
||||||
*
|
*
|
||||||
* @param orderSourceData 前端传入的图片地址 JSON 数组字符串
|
* @param orderSourceData 前端传入的图片地址 JSON 数组字符串
|
||||||
* @param order 当前订单(校验通过时回填实名信息)
|
* @param order 当前订单(识别成功时回填实名信息)
|
||||||
* @return 校验失败返回 ApiResult(fail),校验通过返回 null
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* 身份证 OCR 真实性校验(当前为「尽力而为」模式:OCR 调不通/未通过时仅记录日志,不阻断业务)。
|
|
||||||
* 等阿里云市场接口/AppCode 恢复正常后,本方法会在识别成功时自动回填实名信息,无需额外改动。
|
|
||||||
* orderSourceData 约定顺序:[0]=身份证正面、[1]=身份证反面、[2]=人车合照、[3]=车子照片、[4]=安装照片
|
|
||||||
*
|
|
||||||
* @param orderSourceData 前端传入的图片地址 JSON 数组字符串
|
|
||||||
* @param order 当前订单(校验通过时回填实名信息)
|
|
||||||
* @return 始终返回 null(放行业务);OCR 结果仅用于尽力回填
|
* @return 始终返回 null(放行业务);OCR 结果仅用于尽力回填
|
||||||
*/
|
*/
|
||||||
private ApiResult<?> verifyIdCard(String orderSourceData, Order order) {
|
private ApiResult<?> verifyIdCard(String orderSourceData, Order order) {
|
||||||
@@ -472,42 +465,39 @@ public class OpenEquipmentController extends BaseController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 调第三方 OCR 识别(best-effort:异常/失败均仅记录日志,放行业务)
|
// 3. 调百度身份证 OCR 识别(best-effort:失败仅记录日志,放行业务;但成功会回填实名/idCode)
|
||||||
try {
|
try {
|
||||||
String front = ImageUtil.ImageBase64(images.get(0) + "?x-oss-process=image/resize,w_750/quality,Q_80");
|
String front = ImageUtil.ImageBase64(images.get(0) + "?x-oss-process=image/resize,w_750/quality,Q_80");
|
||||||
String back = ImageUtil.ImageBase64(images.get(1) + "?x-oss-process=image/resize,w_750/quality,Q_80");
|
|
||||||
|
|
||||||
IdcardRespVO verify = idcardService.verify(front, back);
|
BaiduIdCardResult ocr = baiduOcrService.recognizeFront(front);
|
||||||
if (verify == null) {
|
if (ocr != null && StringUtils.hasText(ocr.getIdCardNo())) {
|
||||||
log.warn("身份证OCR识别返回为空,跳过校验,业务继续");
|
order.setIdCode(ocr.getIdCardNo());
|
||||||
return null;
|
if (StringUtils.hasText(ocr.getName())) {
|
||||||
}
|
order.setRealName(ocr.getName());
|
||||||
if (!"FP00000".equals(verify.getCode())) {
|
}
|
||||||
log.warn("身份证OCR识别未通过 code={}, message={},跳过校验,业务继续", verify.getCode(), verify.getMessage());
|
if (StringUtils.hasText(ocr.getAddress())) {
|
||||||
return null;
|
order.setAddress(ocr.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功:回填实名信息
|
User user = userService.getById(order.getUserId());
|
||||||
if (StringUtils.hasText(verify.getName()) && StringUtils.hasText(verify.getIdCardNo())) {
|
if (user != null) {
|
||||||
order.setRealName(verify.getName());
|
user.setIdCard(ocr.getIdCardNo());
|
||||||
order.setIdCode(verify.getIdCardNo());
|
if (StringUtils.hasText(ocr.getName())) {
|
||||||
order.setAddress(verify.getAddress());
|
user.setRealName(ocr.getName());
|
||||||
|
}
|
||||||
User user = userService.getById(order.getUserId());
|
if (StringUtils.hasText(ocr.getAddress())) {
|
||||||
if (user != null) {
|
user.setAddress(ocr.getAddress());
|
||||||
user.setRealName(verify.getName());
|
}
|
||||||
user.setIdCard(verify.getIdCardNo());
|
userService.updateUser(user);
|
||||||
user.setAddress(verify.getAddress());
|
}
|
||||||
userService.updateUser(user);
|
log.info("百度OCR识别成功并回填实名 idCardNo={}", ocr.getIdCardNo());
|
||||||
}
|
} else {
|
||||||
log.info("身份证OCR识别成功并回填实名 name={}, idCardNo={}", verify.getName(), verify.getIdCardNo());
|
log.warn("百度OCR未识别到身份证号,业务继续(可由前端手动填写兜底)");
|
||||||
} else {
|
}
|
||||||
log.warn("身份证OCR识别成功但字段缺失,跳过回填,业务继续");
|
} catch (Exception e) {
|
||||||
}
|
log.error("百度OCR识别调用异常,跳过校验,业务继续", e);
|
||||||
} catch (Exception e) {
|
}
|
||||||
log.error("身份证OCR识别调用异常,跳过校验,业务继续", e);
|
return null;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@ApiOperation("确认收货")
|
/*@ApiOperation("确认收货")
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ public class OrderReceiptParam implements Serializable {
|
|||||||
@ApiModelProperty(value = "家庭地址")
|
@ApiModelProperty(value = "家庭地址")
|
||||||
@NotBlank(message = "家庭地址不能为空!")
|
@NotBlank(message = "家庭地址不能为空!")
|
||||||
private String homeAddress;
|
private String homeAddress;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "身份证号(前端手动填写兜底;校验通过时优先使用百度OCR识别结果)")
|
||||||
|
private String idCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://47.119.165.234:3308/open_ws?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
url: jdbc:mysql://8.134.55.105:13306/yunxinwei?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
username: open_ws
|
username: yunxinwei
|
||||||
password: DzAmFiZfPJ6ZGApm
|
password: T4xEYiwTdEmS2ZED
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://1Panel-mysql-LCaM:3306/yunxinwei?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
url: jdbc:mysql://1Panel-mysql-XsWW:3306/yunxinwei?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
username: yunxinwei
|
username: yunxinwei
|
||||||
password: T4xEYiwTdEmS2ZED
|
password: T4xEYiwTdEmS2ZED
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
@@ -30,7 +30,7 @@ socketio:
|
|||||||
# 框架配置
|
# 框架配置
|
||||||
config:
|
config:
|
||||||
# 生产环境接口
|
# 生产环境接口
|
||||||
server-url: https://yxw.wsdns.cn/api
|
server-url: https://yxw-api.websoft.top/api
|
||||||
# server-url: https://127.0.0.1:9090/api
|
# server-url: https://127.0.0.1:9090/api
|
||||||
upload-path: /www/wwwroot/file.ws/
|
upload-path: /www/wwwroot/file.ws/
|
||||||
|
|
||||||
@@ -47,4 +47,4 @@ config:
|
|||||||
cron:
|
cron:
|
||||||
alert: 0 30 9 * * ?
|
alert: 0 30 9 * * ?
|
||||||
profit: 0 0/2 * * * ?
|
profit: 0 0/2 * * * ?
|
||||||
timeoutOrder: 1 0 0 * * ?
|
timeoutOrder: 1 0 0 * * ?
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ socketio:
|
|||||||
# 多环境配置
|
# 多环境配置
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: prod
|
active: dev
|
||||||
# json时间格式设置
|
# json时间格式设置
|
||||||
jackson:
|
jackson:
|
||||||
time-zone: GMT+8
|
time-zone: GMT+8
|
||||||
@@ -103,6 +103,12 @@ config:
|
|||||||
# smsKey: ${SMS_ACCESS_KEY:LTAI5tBWM9dSmEAoQFhNqxqJ}
|
# smsKey: ${SMS_ACCESS_KEY:LTAI5tBWM9dSmEAoQFhNqxqJ}
|
||||||
# smsSecret: ${SMS_SECRET:Dr0BqiKl7eaL1NNKoCd12qKsbgjnum}
|
# smsSecret: ${SMS_SECRET:Dr0BqiKl7eaL1NNKoCd12qKsbgjnum}
|
||||||
|
|
||||||
|
# 百度智能云身份证OCR(确认收货实名识别,密钥仅服务端使用)
|
||||||
|
baidu:
|
||||||
|
ocr:
|
||||||
|
api-key: PbpC2NPq86MYoyhuNphc72fo
|
||||||
|
secret-key: dFpDFlQ7EQComD2vFAUA8Exj2FkZnkHb
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
#logging:
|
#logging:
|
||||||
# level:
|
# level:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- 修复「确认收货」卡在"确认身份":shop_order 缺少 id_code / address 两列。
|
||||||
|
-- real_name 已存在(real_name 重复报错 1060 已经证实),无需再加。
|
||||||
|
-- 后端 OCR 识别到的身份证号无法落库(MyBatis-Plus updateById 会因列不存在而报错)。
|
||||||
|
-- 部署后端新代码【前】必须先在 yunxinwei 库执行本脚本。
|
||||||
|
-- 本脚本幂等安全:基于 INFORMATION_SCHEMA 判断列是否存在再决定是否执行 ALTER;
|
||||||
|
-- 即使重复跑也不会报 1060 Duplicate column。
|
||||||
|
|
||||||
|
-- 1) 先看一眼现状,便于核对
|
||||||
|
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'shop_order'
|
||||||
|
AND COLUMN_NAME IN ('id_code', 'real_name', 'address', 'comments', 'receipt_phone')
|
||||||
|
ORDER BY COLUMN_NAME;
|
||||||
|
|
||||||
|
-- 2) 条件加列:id_code
|
||||||
|
SET @sql_id_code = IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'shop_order' AND COLUMN_NAME = 'id_code') = 0,
|
||||||
|
'ALTER TABLE `shop_order` ADD COLUMN `id_code` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT ''身份证号'' AFTER `comments`',
|
||||||
|
'SELECT ''id_code 已存在,跳过'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql_id_code; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 3) 条件加列:address
|
||||||
|
SET @sql_address = IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'shop_order' AND COLUMN_NAME = 'address') = 0,
|
||||||
|
'ALTER TABLE `shop_order` ADD COLUMN `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT ''详细地址'' AFTER `real_name`',
|
||||||
|
'SELECT ''address 已存在,跳过'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql_address; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 4) 验证最终结果
|
||||||
|
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'shop_order'
|
||||||
|
AND COLUMN_NAME IN ('id_code', 'real_name', 'address')
|
||||||
|
ORDER BY COLUMN_NAME;
|
||||||
Reference in New Issue
Block a user