Files
cunkebao_v3/Moncter/app/service/IdentifierService.php

313 lines
12 KiB
PHP
Raw Permalink Normal View History

2026-01-05 10:16:20 +08:00
<?php
namespace app\service;
use app\repository\UserProfileRepository;
use app\repository\UserPhoneRelationRepository;
use app\service\UserPhoneService;
use app\utils\EncryptionHelper;
use app\utils\IdCardHelper;
use app\utils\LoggerHelper;
use Ramsey\Uuid\Uuid as UuidGenerator;
/**
* 身份解析服务
*
* 职责:
* - 根据手机号解析person_iduser_id
* - 如果找不到,创建临时人
* - 支持身份证绑定,将临时人转为正式人
* - 处理多手机号到同一人的映射
*/
class IdentifierService
{
public function __construct(
protected UserProfileRepository $userProfileRepository,
protected UserPhoneService $userPhoneService
) {
}
/**
* 根据手机号解析用户IDperson_id
*
* 流程:
* 1. 查询手机号关联表找到指定时间点有效的user_id
* 2. 如果找不到,创建临时人并建立关联
*
* @param string $phoneNumber 手机号
* @param \DateTimeInterface|null $atTime 查询时间点(默认为当前时间)
* @return string user_idperson_id
*/
public function resolvePersonIdByPhone(string $phoneNumber, ?\DateTimeInterface $atTime = null): string
{
// 检查手机号是否为空
$trimmedPhone = trim($phoneNumber);
if (empty($trimmedPhone)) {
// 如果手机号为空,创建一个没有手机号的临时用户
$userId = $this->createTemporaryPerson(null, $atTime);
LoggerHelper::logBusiness('temporary_person_created_no_phone', [
'user_id' => $userId,
'note' => '手机号为空,创建无手机号的临时用户',
]);
return $userId;
}
// 1. 先查询手机号关联表(使用指定的时间点)
$userId = $this->userPhoneService->findUserByPhone($trimmedPhone, $atTime);
if ($userId !== null) {
LoggerHelper::logBusiness('person_resolved_by_phone', [
'phone_number' => $trimmedPhone,
'user_id' => $userId,
'source' => 'existing_relation',
'at_time' => $atTime ? $atTime->format('Y-m-d H:i:s') : null,
]);
return $userId;
}
// 2. 如果找不到创建临时人使用atTime作为生效时间
$userId = $this->createTemporaryPerson($trimmedPhone, $atTime);
LoggerHelper::logBusiness('temporary_person_created', [
'phone_number' => $trimmedPhone,
'user_id' => $userId,
'effective_time' => $atTime ? $atTime->format('Y-m-d H:i:s') : null,
]);
return $userId;
}
/**
* 根据身份证解析用户IDperson_id
*
* @param string $idCard 身份证号
* @return string|null user_idperson_id如果不存在返回null
*/
public function resolvePersonIdByIdCard(string $idCard): ?string
{
$idCardHash = EncryptionHelper::hash($idCard);
$user = $this->userProfileRepository->findByIdCardHash($idCardHash);
if ($user) {
LoggerHelper::logBusiness('person_resolved_by_id_card', [
'id_card_hash' => $idCardHash,
'user_id' => $user->user_id,
]);
return $user->user_id;
}
return null;
}
/**
* 绑定身份证到用户(将临时人转为正式人,或创建正式人)
*
* @param string $userId 用户ID
* @param string $idCard 身份证号
* @return bool 是否成功
* @throws \InvalidArgumentException
*/
public function bindIdCardToPerson(string $userId, string $idCard): bool
{
$idCardHash = EncryptionHelper::hash($idCard);
$idCardEncrypted = EncryptionHelper::encrypt($idCard);
// 检查该身份证是否已被其他用户使用
$existingUser = $this->userProfileRepository->findByIdCardHash($idCardHash);
if ($existingUser && $existingUser->user_id !== $userId) {
throw new \InvalidArgumentException("身份证号已被其他用户使用user_id: {$existingUser->user_id}");
}
// 更新用户信息
$user = $this->userProfileRepository->findByUserId($userId);
if (!$user) {
throw new \InvalidArgumentException("用户不存在: {$userId}");
}
// 如果用户已经是正式人且身份证匹配,无需更新
if (!$user->is_temporary && $user->id_card_hash === $idCardHash) {
return true;
}
// 更新身份证信息并标记为正式人
$user->id_card_hash = $idCardHash;
$user->id_card_encrypted = $idCardEncrypted;
$user->id_card_type = '身份证';
$user->is_temporary = false;
// 从身份证号中自动提取基础信息(如果字段为空才更新)
$idCardInfo = IdCardHelper::extractInfo($idCard);
if ($idCardInfo['birthday'] !== null && $user->birthday === null) {
$user->birthday = $idCardInfo['birthday'];
}
// 只有当性别解析成功且当前值为 null 时才更新0 也被认为是未设置)
if ($idCardInfo['gender'] > 0 && ($user->gender === null || $user->gender === 0)) {
$user->gender = $idCardInfo['gender'];
}
$user->update_time = new \DateTimeImmutable('now');
$user->save();
LoggerHelper::logBusiness('id_card_bound_to_person', [
'user_id' => $userId,
'id_card_hash' => $idCardHash,
'was_temporary' => $user->is_temporary ?? true,
]);
return true;
}
/**
* 创建临时人
*
* @param string|null $phoneNumber 手机号(可选,用于建立关联)
* @param \DateTimeInterface|null $effectiveTime 生效时间(用于手机关联,默认当前时间)
* @return string user_id
*/
private function createTemporaryPerson(?string $phoneNumber = null, ?\DateTimeInterface $effectiveTime = null): string
{
$now = new \DateTimeImmutable('now');
$userId = UuidGenerator::uuid4()->toString();
// 创建临时人记录
$user = new UserProfileRepository();
$user->user_id = $userId;
$user->is_temporary = true;
$user->status = 0;
$user->total_amount = 0;
$user->total_count = 0;
$user->create_time = $now;
$user->update_time = $now;
$user->save();
// 如果有手机号建立关联使用effectiveTime作为生效时间
// 检查手机号不为空null 或空字符串都跳过)
if ($phoneNumber !== null && trim($phoneNumber) !== '') {
try {
$trimmedPhone = trim($phoneNumber);
$this->userPhoneService->addPhoneToUser($userId, $trimmedPhone, [
'source' => 'auto_created',
'type' => 'personal',
'effective_time' => $effectiveTime ?? $now,
]);
LoggerHelper::logBusiness('phone_relation_created_success', [
'user_id' => $userId,
'phone_number' => $trimmedPhone,
'effective_time' => ($effectiveTime ?? $now)->format('Y-m-d H:i:s'),
]);
} catch (\Throwable $e) {
// 手机号关联失败不影响用户创建,只记录详细的错误日志
LoggerHelper::logError($e, [
'component' => 'IdentifierService',
'action' => 'createTemporaryPerson',
'user_id' => $userId,
'phone_number' => $phoneNumber,
'phone_number_length' => strlen($phoneNumber),
'error_message' => $e->getMessage(),
'error_type' => get_class($e),
]);
// 同时记录业务日志,便于排查
LoggerHelper::logBusiness('phone_relation_create_failed', [
'user_id' => $userId,
'phone_number' => $phoneNumber,
'error_message' => $e->getMessage(),
'note' => '用户已创建,但手机关联失败',
]);
}
} elseif ($phoneNumber !== null && trim($phoneNumber) === '') {
// 手机号是空字符串,记录日志
LoggerHelper::logBusiness('phone_relation_skipped_empty', [
'user_id' => $userId,
'note' => '手机号为空字符串,跳过关联创建',
]);
}
return $userId;
}
/**
* 根据手机号或身份证解析用户ID
*
* 优先级:身份证 > 手机号
*
* @param string|null $phoneNumber 手机号
* @param string|null $idCard 身份证号
* @param \DateTimeInterface|null $atTime 查询时间点(用于手机号查询,默认为当前时间)
* @return string user_id
*/
public function resolvePersonId(?string $phoneNumber = null, ?string $idCard = null, ?\DateTimeInterface $atTime = null): string
{
$atTime = $atTime ?? new \DateTimeImmutable('now');
// 优先使用身份证
if ($idCard !== null && !empty($idCard)) {
$userId = $this->resolvePersonIdByIdCard($idCard);
if ($userId !== null) {
// 如果身份证存在,但提供了手机号,确保手机号关联到该用户
if ($phoneNumber !== null && !empty($phoneNumber)) {
// 在atTime时间点查询手机号关联
$existingUserId = $this->userPhoneService->findUserByPhone($phoneNumber, $atTime);
if ($existingUserId === null) {
// 手机号未关联建立关联使用atTime作为生效时间
$this->userPhoneService->addPhoneToUser($userId, $phoneNumber, [
'source' => 'id_card_resolved',
'type' => 'personal',
'effective_time' => $atTime,
]);
} elseif ($existingUserId !== $userId) {
// 手机号已关联到其他用户需要合并由PersonMergeService处理
LoggerHelper::logBusiness('phone_bound_to_different_person', [
'phone_number' => $phoneNumber,
'existing_user_id' => $existingUserId,
'id_card_user_id' => $userId,
'at_time' => $atTime->format('Y-m-d H:i:s'),
]);
}
}
return $userId;
} else {
// 身份证不存在,但有身份证信息,创建一个临时用户并绑定身份证(使其成为正式用户)
$userId = $this->createTemporaryPerson($phoneNumber, $atTime);
try {
$this->bindIdCardToPerson($userId, $idCard);
} catch (\Throwable $e) {
// 绑定失败不影响返回user_id
LoggerHelper::logError($e, [
'component' => 'IdentifierService',
'action' => 'resolvePersonId',
'user_id' => $userId,
]);
}
return $userId;
}
}
// 使用手机号传入atTime
if ($phoneNumber !== null && !empty($phoneNumber)) {
$userId = $this->resolvePersonIdByPhone($phoneNumber, $atTime);
// 如果同时提供了身份证,绑定身份证
if ($idCard !== null && !empty($idCard)) {
try {
$this->bindIdCardToPerson($userId, $idCard);
} catch (\Throwable $e) {
// 绑定失败不影响返回user_id
LoggerHelper::logError($e, [
'component' => 'IdentifierService',
'action' => 'resolvePersonId',
'user_id' => $userId,
]);
}
}
return $userId;
}
// 都没有提供,创建临时人
return $this->createTemporaryPerson(null, $atTime);
}
}