Files
cunkebao_v3/Moncter/app/service/IdentifierService.php
2026-01-05 10:16:20 +08:00

313 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}