diff --git a/pom.xml b/pom.xml index 3cd2de5..b3d2e72 100644 --- a/pom.xml +++ b/pom.xml @@ -213,6 +213,12 @@ aliyun-java-sdk-core 4.4.3 + + + com.aliyun + aliyun-java-sdk-ocr + 1.0.6 + com.alipay.sdk diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index 2bc6d2a..43e235a 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -82,7 +82,10 @@ public class SecurityConfig { "/api/shop/order-delivery/notify", "/api/hjc/push/project", "/api/hjc/auth/register", - "/api/hjc/auth/login" + "/api/hjc/auth/login", + // 注册页专用:证件上传与 OCR 识别在登录前发生 + "/api/hjc/auth/upload", + "/api/hjc/ocr/recognize" ) .permitAll() .anyRequest() diff --git a/src/main/java/com/gxwebsoft/hjc/controller/HjcAuthController.java b/src/main/java/com/gxwebsoft/hjc/controller/HjcAuthController.java index ef50849..b49a405 100644 --- a/src/main/java/com/gxwebsoft/hjc/controller/HjcAuthController.java +++ b/src/main/java/com/gxwebsoft/hjc/controller/HjcAuthController.java @@ -1,10 +1,13 @@ package com.gxwebsoft.hjc.controller; import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.gxwebsoft.common.core.config.ConfigProperties; import com.gxwebsoft.common.core.security.JwtSubject; import com.gxwebsoft.common.core.security.JwtUtil; +import com.gxwebsoft.common.core.utils.FileServerUtil; +import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.system.entity.Role; @@ -16,6 +19,8 @@ import com.gxwebsoft.common.system.service.UserRoleService; import com.gxwebsoft.common.system.service.UserService; import com.gxwebsoft.hjc.dto.HjcAuthRequest; import com.gxwebsoft.hjc.entity.HjcEnterprise; +import com.gxwebsoft.hjc.entity.HjcEnterpriseMaterial; +import com.gxwebsoft.hjc.service.HjcEnterpriseMaterialService; import com.gxwebsoft.hjc.service.HjcEnterpriseService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -23,9 +28,16 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * 汇吉采 网站企业 注册/登录(企业名称+密码) @@ -45,14 +57,61 @@ public class HjcAuthController extends BaseController { private ConfigProperties configProperties; @Resource private HjcEnterpriseService hjcEnterpriseService; + @Resource + private HjcEnterpriseMaterialService hjcEnterpriseMaterialService; + @Resource + private RedisUtil redisUtil; - @Operation(summary = "企业注册(企业名称+密码,注册后待审核)") + /** 注册必传的资质证件材料类型 */ + private static final List REQUIRED_MATERIAL_TYPES = + Arrays.asList("idcard_front", "idcard_back", "handbook", "license"); + + @Operation(summary = "注册证件上传(匿名,注册页专用,不写文件表)") + @PostMapping("/upload") + public ApiResult> upload(@RequestParam("file") MultipartFile file) { + if (file == null || file.isEmpty()) { + return fail("上传文件不能为空", null); + } + try { + String dir = configProperties.getUploadPath(); + File upload = FileServerUtil.upload(file, dir, Boolean.TRUE.equals(configProperties.getUploadUuidName())); + String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1); + // 默认走自建文件服务器 + String url = configProperties.getFileServer() + "/api/file" + path; + // 云存储(OSS)时改用 bucketDomain + Integer tenantId = getTenantId(); + if (tenantId != null) { + JSONObject setting = JSONObject.parseObject(redisUtil.get("setting:upload:" + tenantId)); + if (setting != null) { + String uploadMethod = setting.getString("uploadMethod"); + String bucketDomain = setting.getString("bucketDomain"); + if (StrUtil.isNotBlank(uploadMethod) && !"file".equals(uploadMethod) + && StrUtil.isNotBlank(bucketDomain)) { + url = bucketDomain + path; + } + } + } + Map data = new HashMap<>(4); + data.put("url", url); + data.put("name", file.getOriginalFilename()); + return success(data); + } catch (Exception e) { + e.printStackTrace(); + return fail("上传失败", null); + } + } + + @Operation(summary = "企业注册(一站式:企业+经办人+授权+证件,提交后待审核)") @PostMapping("/register") @Transactional(rollbackFor = Exception.class) public ApiResult register(@RequestBody HjcAuthRequest request) { if (StrUtil.isBlank(request.getEnterpriseName()) || StrUtil.isBlank(request.getPassword())) { return fail("企业名称和密码不能为空", null); } + String invalid = validateRegister(request); + if (invalid != null) { + return fail(invalid, null); + } Integer tenantId = getTenantId(); if (tenantId == null) { return fail("租户ID不能为空", null); @@ -97,16 +156,69 @@ public class HjcAuthController extends BaseController { enterprise.setAgentName(request.getAgentName()); enterprise.setAgentEmail(request.getAgentEmail()); enterprise.setAgentPhone(request.getAgentPhone()); + enterprise.setIdCardNo(request.getIdCardNo()); enterprise.setAuthorizeExpire(request.getAuthorizeExpire()); enterprise.setAuthStatus(0); enterprise.setTenantId(tenantId); hjcEnterpriseService.save(enterprise); + // 一站式注册:同事务保存资质证件材料 + List materials = request.getMaterials(); + if (materials != null) { + for (HjcEnterpriseMaterial m : materials) { + if (StrUtil.isBlank(m.getFileUrl())) { + continue; + } + m.setId(null); + m.setEnterpriseId(enterprise.getId()); + m.setTenantId(tenantId); + hjcEnterpriseMaterialService.save(m); + } + } + String token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), configProperties.getTokenExpireTime(), configProperties.getTokenKey()); return success("注册成功", new LoginResult(token, user)); } + /** + * 注册必填校验:企业联系电话、企业地址选填,其余必填;4 类证件必传 + * + * @return 错误信息;校验通过返回 null + */ + private String validateRegister(HjcAuthRequest request) { + if (StrUtil.isBlank(request.getCreditCode())) { + return "纳税人识别号不能为空"; + } + if (StrUtil.isBlank(request.getContactEmail())) { + return "企业邮箱不能为空"; + } + if (StrUtil.isBlank(request.getAgentName())) { + return "经办人姓名不能为空"; + } + if (StrUtil.isBlank(request.getAgentEmail())) { + return "经办人邮箱不能为空"; + } + if (StrUtil.isBlank(request.getAgentPhone())) { + return "经办人手机号不能为空"; + } + if (StrUtil.isBlank(request.getIdCardNo())) { + return "经办人身份证号不能为空"; + } + if (request.getAuthorizeExpire() == null) { + return "授权到期时间不能为空"; + } + List materials = request.getMaterials(); + for (String type : REQUIRED_MATERIAL_TYPES) { + boolean present = materials != null && materials.stream() + .anyMatch(m -> type.equals(m.getMaterialType()) && StrUtil.isNotBlank(m.getFileUrl())); + if (!present) { + return "请上传全部证件(身份证正反面、授权委托书、营业执照)"; + } + } + return null; + } + @Operation(summary = "企业登录(企业名称+密码)") @PostMapping("/login") public ApiResult login(@RequestBody HjcAuthRequest request) { diff --git a/src/main/java/com/gxwebsoft/hjc/controller/HjcOcrController.java b/src/main/java/com/gxwebsoft/hjc/controller/HjcOcrController.java new file mode 100644 index 0000000..339897e --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/controller/HjcOcrController.java @@ -0,0 +1,41 @@ +package com.gxwebsoft.hjc.controller; + +import cn.hutool.core.util.StrUtil; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.hjc.dto.HjcOcrRecognizeRequest; +import com.gxwebsoft.hjc.dto.HjcOcrResult; +import com.gxwebsoft.hjc.service.HjcOcrService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 汇吉采 证件 OCR 识别(注册页在登录前调用,SecurityConfig 已匿名放行) + */ +@Tag(name = "汇吉采-证件OCR") +@RestController +@RequestMapping("/api/hjc/ocr") +public class HjcOcrController extends BaseController { + + @Resource + private HjcOcrService hjcOcrService; + + @Operation(summary = "识别证件(身份证人像面/营业执照),回填表单字段") + @PostMapping("/recognize") + public ApiResult recognize(@RequestBody HjcOcrRecognizeRequest request) { + if (StrUtil.isBlank(request.getMaterialType()) || StrUtil.isBlank(request.getFileUrl())) { + return fail("materialType 与 fileUrl 不能为空", null); + } + HjcOcrResult result = hjcOcrService.recognize(request.getMaterialType(), request.getFileUrl()); + if (result == null) { + return fail("识别失败或该类型无需识别,请手动填写", null); + } + return success(result); + } +} diff --git a/src/main/java/com/gxwebsoft/hjc/dto/HjcAuthRequest.java b/src/main/java/com/gxwebsoft/hjc/dto/HjcAuthRequest.java index 8dcce33..eaffafe 100644 --- a/src/main/java/com/gxwebsoft/hjc/dto/HjcAuthRequest.java +++ b/src/main/java/com/gxwebsoft/hjc/dto/HjcAuthRequest.java @@ -1,10 +1,12 @@ package com.gxwebsoft.hjc.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import com.gxwebsoft.hjc.entity.HjcEnterpriseMaterial; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; /** * 汇吉采 网站企业 注册/登录 请求(企业名称+密码) @@ -40,7 +42,13 @@ public class HjcAuthRequest { @Schema(description = "经办人手机号") private String agentPhone; + @Schema(description = "经办人身份证号") + private String idCardNo; + @Schema(description = "授权委托书到期时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime authorizeExpire; + + @Schema(description = "资质证件材料") + private List materials; } diff --git a/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrRecognizeRequest.java b/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrRecognizeRequest.java new file mode 100644 index 0000000..dfdb5e0 --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrRecognizeRequest.java @@ -0,0 +1,18 @@ +package com.gxwebsoft.hjc.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 汇吉采 证件 OCR 识别请求 + */ +@Data +@Schema(name = "HjcOcrRecognizeRequest", description = "汇吉采证件OCR识别请求") +public class HjcOcrRecognizeRequest { + + @Schema(description = "材料类型:idcard_front(身份证人像面)/ license(营业执照)", required = true) + private String materialType; + + @Schema(description = "已上传证件的可访问地址(fileUrl)", required = true) + private String fileUrl; +} diff --git a/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrResult.java b/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrResult.java new file mode 100644 index 0000000..330830b --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/dto/HjcOcrResult.java @@ -0,0 +1,36 @@ +package com.gxwebsoft.hjc.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 汇吉采 证件 OCR 识别结果 + * + *

字段按材料类型填写:营业执照填 enterpriseName/creditCode/address/legalPerson; + * 身份证人像面填 agentName/idCardNo;未识别到的字段为 null。

+ */ +@Data +@Schema(name = "HjcOcrResult", description = "汇吉采证件OCR识别结果") +public class HjcOcrResult { + + @Schema(description = "材料类型:idcard_front / license") + private String materialType; + + @Schema(description = "营业执照-企业名称(仅作只读核对,不覆盖登录账号)") + private String enterpriseName; + + @Schema(description = "营业执照-统一社会信用代码") + private String creditCode; + + @Schema(description = "营业执照-住所/企业地址") + private String address; + + @Schema(description = "营业执照-法定代表人(仅展示供人工参考)") + private String legalPerson; + + @Schema(description = "身份证-经办人姓名") + private String agentName; + + @Schema(description = "身份证-经办人身份证号") + private String idCardNo; +} diff --git a/src/main/java/com/gxwebsoft/hjc/entity/HjcEnterprise.java b/src/main/java/com/gxwebsoft/hjc/entity/HjcEnterprise.java index 165135e..83cd57c 100644 --- a/src/main/java/com/gxwebsoft/hjc/entity/HjcEnterprise.java +++ b/src/main/java/com/gxwebsoft/hjc/entity/HjcEnterprise.java @@ -55,6 +55,9 @@ public class HjcEnterprise implements Serializable { @Schema(description = "经办人手机号") private String agentPhone; + @Schema(description = "经办人身份证号") + private String idCardNo; + @Schema(description = "授权委托书到期时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime authorizeExpire; diff --git a/src/main/java/com/gxwebsoft/hjc/service/HjcOcrService.java b/src/main/java/com/gxwebsoft/hjc/service/HjcOcrService.java new file mode 100644 index 0000000..e7bfd43 --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/service/HjcOcrService.java @@ -0,0 +1,18 @@ +package com.gxwebsoft.hjc.service; + +import com.gxwebsoft.hjc.dto.HjcOcrResult; + +/** + * 汇吉采 证件 OCR 识别(阿里云文字识别 OCR,服务端调用) + */ +public interface HjcOcrService { + + /** + * 识别证件 + * + * @param materialType 材料类型:idcard_front(身份证人像面)/ license(营业执照) + * @param fileUrl 已上传证件的可访问地址 + * @return 识别结果;不支持的材料类型或识别失败返回 null + */ + HjcOcrResult recognize(String materialType, String fileUrl); +} diff --git a/src/main/java/com/gxwebsoft/hjc/service/impl/HjcOcrServiceImpl.java b/src/main/java/com/gxwebsoft/hjc/service/impl/HjcOcrServiceImpl.java new file mode 100644 index 0000000..373e1ff --- /dev/null +++ b/src/main/java/com/gxwebsoft/hjc/service/impl/HjcOcrServiceImpl.java @@ -0,0 +1,107 @@ +package com.gxwebsoft.hjc.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.ocr.model.v20191230.RecognizeBusinessLicenseRequest; +import com.aliyuncs.ocr.model.v20191230.RecognizeBusinessLicenseResponse; +import com.aliyuncs.ocr.model.v20191230.RecognizeIdentityCardRequest; +import com.aliyuncs.ocr.model.v20191230.RecognizeIdentityCardResponse; +import com.aliyuncs.profile.DefaultProfile; +import com.gxwebsoft.hjc.dto.HjcOcrResult; +import com.gxwebsoft.hjc.service.HjcOcrService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 汇吉采 证件 OCR 识别实现(阿里云文字识别 OCR) + * + *

AK 需具备 OCR 服务权限;未配置(占位值为空)时直接返回 null,由前端提示「识别失败,请手动填写」, + * 不阻断注册流程。

+ */ +@Service +public class HjcOcrServiceImpl implements HjcOcrService { + + /** 材料类型:身份证人像面 */ + public static final String MATERIAL_IDCARD_FRONT = "idcard_front"; + /** 材料类型:营业执照 */ + public static final String MATERIAL_LICENSE = "license"; + + @Value("${aliyun.ocr.access-key-id:}") + private String accessKeyId; + + @Value("${aliyun.ocr.access-key-secret:}") + private String accessKeySecret; + + @Value("${aliyun.ocr.region:cn-hangzhou}") + private String region; + + @Value("${aliyun.ocr.endpoint:ocr-api.cn-hangzhou.aliyuncs.com}") + private String endpoint; + + @Override + public HjcOcrResult recognize(String materialType, String fileUrl) { + if (StrUtil.isBlank(materialType) || StrUtil.isBlank(fileUrl)) { + return null; + } + if (StrUtil.isBlank(accessKeyId) || StrUtil.isBlank(accessKeySecret)) { + // OCR 未配置 AK(占位),不阻断注册 + System.out.println("HjcOcr: aliyun.ocr 未配置 AccessKey,跳过识别"); + return null; + } + try { + IAcsClient client = createClient(); + if (MATERIAL_IDCARD_FRONT.equals(materialType)) { + return recognizeIdCard(client, fileUrl); + } + if (MATERIAL_LICENSE.equals(materialType)) { + return recognizeBusinessLicense(client, fileUrl); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private IAcsClient createClient() throws Exception { + DefaultProfile profile = DefaultProfile.getProfile(region, accessKeyId, accessKeySecret); + DefaultProfile.addEndpoint(region, region, "ocr", endpoint); + return new DefaultAcsClient(profile); + } + + /** 身份证人像面:回填经办人姓名 + 身份证号 */ + private HjcOcrResult recognizeIdCard(IAcsClient client, String fileUrl) throws Exception { + RecognizeIdentityCardRequest request = new RecognizeIdentityCardRequest(); + request.setImageURL(fileUrl); + request.setSide("face"); + RecognizeIdentityCardResponse response = client.getAcsResponse(request); + if (response == null || response.getData() == null + || response.getData().getFrontResult() == null) { + return null; + } + RecognizeIdentityCardResponse.Data.FrontResult front = response.getData().getFrontResult(); + HjcOcrResult result = new HjcOcrResult(); + result.setMaterialType(MATERIAL_IDCARD_FRONT); + result.setAgentName(front.getName()); + result.setIdCardNo(front.getIDNumber()); + return result; + } + + /** 营业执照:回填统一社会信用代码 + 企业地址;企业名称/法定代表人仅作展示提示 */ + private HjcOcrResult recognizeBusinessLicense(IAcsClient client, String fileUrl) throws Exception { + RecognizeBusinessLicenseRequest request = new RecognizeBusinessLicenseRequest(); + request.setImageURL(fileUrl); + RecognizeBusinessLicenseResponse response = client.getAcsResponse(request); + if (response == null || response.getData() == null) { + return null; + } + RecognizeBusinessLicenseResponse.Data data = response.getData(); + HjcOcrResult result = new HjcOcrResult(); + result.setMaterialType(MATERIAL_LICENSE); + result.setEnterpriseName(data.getName()); + result.setCreditCode(data.getRegisterNumber()); + result.setAddress(data.getAddress()); + result.setLegalPerson(data.getLegalPerson()); + return result; + } +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7c17894..1073da1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -77,3 +77,9 @@ aliyun: access-key-id: LTAI5tEsyhW4GCKbds1qsopg access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO endpoint: mt.cn-hangzhou.aliyuncs.com + # 阿里云文字识别 OCR(身份证/营业执照识别),AK 需具备 OCR 服务权限;未开通前为占位值 + ocr: + access-key-id: "" + access-key-secret: "" + region: cn-hangzhou + endpoint: ocr-api.cn-hangzhou.aliyuncs.com diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3c1c894..57246f8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -105,6 +105,14 @@ config: bucketDomain: https://oss.wsdns.cn aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com +# 阿里云文字识别 OCR(身份证/营业执照识别),AK 需具备 OCR 服务权限;未开通前为占位值 +aliyun: + ocr: + access-key-id: "" + access-key-secret: "" + region: cn-hangzhou + endpoint: ocr-api.cn-hangzhou.aliyuncs.com + # 商城订单配置 shop: order: diff --git a/src/main/resources/sql/hjc_enterprise_add_id_card_no.sql b/src/main/resources/sql/hjc_enterprise_add_id_card_no.sql new file mode 100644 index 0000000..79c1128 --- /dev/null +++ b/src/main/resources/sql/hjc_enterprise_add_id_card_no.sql @@ -0,0 +1,3 @@ +-- 汇吉采:企业资质新增「经办人身份证号」字段(注册/资质认证页,身份证人像面 OCR 回填) +ALTER TABLE `hjc_enterprise` + ADD COLUMN `id_card_no` varchar(64) DEFAULT NULL COMMENT '经办人身份证号' AFTER `agent_phone`; diff --git a/src/main/resources/sql/hjc_init.sql b/src/main/resources/sql/hjc_init.sql index 7d85dfd..20cfbe9 100644 --- a/src/main/resources/sql/hjc_init.sql +++ b/src/main/resources/sql/hjc_init.sql @@ -51,6 +51,7 @@ CREATE TABLE `hjc_enterprise` ( `agent_name` varchar(64) DEFAULT NULL COMMENT '经办人姓名', `agent_email` varchar(128) DEFAULT NULL COMMENT '经办人邮箱', `agent_phone` varchar(32) DEFAULT NULL COMMENT '经办人手机号', + `id_card_no` varchar(64) DEFAULT NULL COMMENT '经办人身份证号', `authorize_expire` datetime DEFAULT NULL COMMENT '授权委托书到期时间', `auth_status` tinyint(4) DEFAULT 0 COMMENT '资质状态:0待审核 1已通过 2已驳回', `reject_reason` varchar(500) DEFAULT NULL COMMENT '驳回原因',