This commit is contained in:
Your Name
2026-04-30 09:27:42 +08:00
parent dc899e4af4
commit eb782305e6
460 changed files with 10785 additions and 928 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ class ExpressAutoUpdate extends Command
$startTime = microtime(true);
try {
$result = ExpressTrackingService::autoUpdateBatch(50);
$result = ExpressTrackingService::autoUpdateBatch(150);
$duration = round(microtime(true) - $startTime, 2);
@@ -0,0 +1,358 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\adminapi\logic\qywx\CustomerLogic;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Db;
/**
* 一次性把 zyt_qywx_external_contact.follow_users JSON 中的 tags
* 1. 合并去重后回填到 zyt_qywx_external_contact.tagsJSON 字段,详情页用)
* 2. 拍平按 (external_userid, follow_user_id, tag_id) 三元组同步到关系表
* zyt_qywx_external_contact_tag(用于检索/统计/聚合)
*
* 使用方法:
* php think qywx:backfill-customer-tags
* php think qywx:backfill-customer-tags --all (强制刷新所有行,不仅是 tags 为空的)
*
* 不调企微 API、纯本地解析;新加 tags 字段或关系表后跑一次即可(后续 UPSERT 自动维护)。
*/
class QywxBackfillCustomerTags extends Command
{
protected function configure()
{
$this->setName('qywx:backfill-customer-tags')
->addOption(
'all',
'a',
\think\console\input\Option::VALUE_NONE,
'强制刷新所有行(默认只处理 tags 为空 / NULL / [] 的行)'
)
->addOption(
'fast',
'f',
\think\console\input\Option::VALUE_NONE,
'快速模式:批量 INSERT IGNORE 关系表,CASE-WHEN 批量 UPDATE tags(首次回填/远程库网络延迟时用)'
)
->setDescription('回填外部联系人 tags 字段(从本地 follow_users JSON 提取)');
}
protected function execute(Input $input, Output $output)
{
$all = (bool) $input->getOption('all');
$fast = (bool) $input->getOption('fast');
$startTime = microtime(true);
if ($fast) {
return $this->executeFast($input, $output, $all, $startTime);
}
$output->writeln('开始回填 qywx_external_contact.tags ...');
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行'));
$query = Db::name('qywx_external_contact')
->whereNull('delete_time')
->where('follow_users', '<>', '')
->where('follow_users', '<>', '[]');
if (!$all) {
$query->where(function ($q) {
$q->whereNull('tags')
->whereOr('tags', '')
->whereOr('tags', '[]');
});
}
$total = (int) (clone $query)->count();
$output->writeln("候选 {$total}");
if ($total === 0) {
$output->writeln('无需回填');
return 0;
}
$processed = 0;
$updated = 0;
$unchanged = 0;
$emptyTags = 0;
$relationSynced = 0;
// 分页处理避免内存爆
$pageSize = 500;
$lastId = 0;
while (true) {
$rows = (clone $query)
->where('id', '>', $lastId)
->order('id', 'asc')
->limit($pageSize)
->field(['id', 'external_userid', 'follow_users', 'tags'])
->select()
->toArray();
if ($rows === []) {
break;
}
// 拿到本批 id 对应 external_userid,用于同步关系表
$idToExt = [];
foreach ($rows as $row) {
$idToExt[(int) $row['id']] = (string) ($row['external_userid'] ?? '');
}
foreach ($rows as $row) {
$lastId = (int) $row['id'];
$processed++;
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
if (!is_array($followUsers)) {
$followUsers = [];
}
// —— 关系表(每行都同步,不依赖 JSON 字段是否变化;--all 模式下也会全量重写)
$extId = $idToExt[$lastId] ?? '';
if ($extId !== '') {
CustomerLogic::syncContactTagsRelation($extId, $followUsers);
$relationSynced++;
}
// —— tags JSON 字段(值未变的跳过 UPDATE,省 IO)
$newTags = CustomerLogic::extractFollowUserTags($followUsers);
$oldTags = (string) ($row['tags'] ?? '');
if ($newTags === '[]') {
$emptyTags++;
}
if ($newTags === $oldTags) {
$unchanged++;
continue;
}
Db::name('qywx_external_contact')
->where('id', $lastId)
->update([
'tags' => $newTags,
// 不刷 update_time,避免误触发"最近活跃"类排序
]);
$updated++;
}
if (($processed % 2000) === 0) {
$output->writeln(sprintf('进度: %d / %d,已更新 %d', $processed, $total, $updated));
}
}
$duration = round(microtime(true) - $startTime, 2);
$output->writeln('');
$output->writeln('========================================');
$output->writeln('回填完成');
$output->writeln('========================================');
$output->writeln("处理: {$processed}");
$output->writeln("tags JSON 更新: {$updated}");
$output->writeln("tags JSON 未变: {$unchanged}");
$output->writeln("空 tags 行数: {$emptyTags} follow_user 内无任何 tag");
$output->writeln("关系表同步: {$relationSynced}");
$output->writeln("耗时: {$duration}");
return 0;
}
/**
* 快速模式:批量 INSERT IGNORE + 批量 CASE-WHEN UPDATE,远程库网络延迟下推荐用此模式。
* 注意:不会删除已在关系表中、但当前 follow_users 已不再存在的"过时"关系;首次回填场景安全。
*/
private function executeFast(Input $input, Output $output, bool $all, float $startTime): int
{
$output->writeln('开始[快速]回填 qywx_external_contact.tags ...');
$output->writeln('模式: ' . ($all ? '全量刷新' : '仅刷 tags 为空的行') . ' + fast');
$query = Db::name('qywx_external_contact')
->whereNull('delete_time')
->where('follow_users', '<>', '')
->where('follow_users', '<>', '[]');
if (!$all) {
$query->where(function ($q) {
$q->whereNull('tags')
->whereOr('tags', '')
->whereOr('tags', '[]');
});
}
$total = (int) (clone $query)->count();
$output->writeln("候选 {$total}");
if ($total === 0) {
$output->writeln('无需回填');
return 0;
}
$processed = 0;
$tagRowsInserted = 0;
$jsonUpdated = 0;
$pageSize = 1000;
$lastId = 0;
$now = time();
while (true) {
$rows = (clone $query)
->where('id', '>', $lastId)
->order('id', 'asc')
->limit($pageSize)
->field(['id', 'external_userid', 'follow_users'])
->select()
->toArray();
if ($rows === []) {
break;
}
$tagBatch = [];
$tagJsonByExtId = [];
foreach ($rows as $row) {
$lastId = (int) $row['id'];
$processed++;
$extId = (string) ($row['external_userid'] ?? '');
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
if (!is_array($followUsers)) {
$followUsers = [];
}
$tagJsonByExtId[$lastId] = CustomerLogic::extractFollowUserTags($followUsers);
if ($extId === '') {
continue;
}
foreach ($followUsers as $fu) {
if (!is_array($fu)) {
continue;
}
$followUserId = mb_substr(trim((string) ($fu['userid'] ?? '')), 0, 64);
$tags = $fu['tags'] ?? [];
if (!is_array($tags)) {
continue;
}
foreach ($tags as $t) {
if (!is_array($t)) {
continue;
}
$tagId = mb_substr(trim((string) ($t['tag_id'] ?? '')), 0, 64);
if ($tagId === '') {
continue;
}
$tagBatch[] = [
'external_userid' => $extId,
'follow_user_id' => $followUserId,
'tag_id' => $tagId,
'tag_name' => mb_substr((string) ($t['tag_name'] ?? ''), 0, 128),
'group_name' => mb_substr((string) ($t['group_name'] ?? ''), 0, 128),
'type' => isset($t['type']) ? (int) $t['type'] : 1,
'create_time' => $now,
'update_time' => $now,
];
}
}
}
if ($tagBatch !== []) {
$tagRowsInserted += $this->batchInsertIgnoreTags($tagBatch);
}
if ($tagJsonByExtId !== []) {
$jsonUpdated += $this->batchUpdateTagsJson($tagJsonByExtId);
}
$output->writeln(sprintf('进度: %d / %d 关系累计 %d tags JSON 累计 %d', $processed, $total, $tagRowsInserted, $jsonUpdated));
}
$duration = round(microtime(true) - $startTime, 2);
$output->writeln('');
$output->writeln('========================================');
$output->writeln('[快速]回填完成');
$output->writeln('========================================');
$output->writeln("处理: {$processed}");
$output->writeln("关系表 INSERT IGNORE: {$tagRowsInserted}(含可能被忽略的重复行)");
$output->writeln("tags JSON 批量 UPDATE: {$jsonUpdated}");
$output->writeln("耗时: {$duration}");
return 0;
}
/**
* 批量 INSERT IGNORE 到关系表。返回受影响(实际新插入)行数。
*
* @param array<int, array<string, mixed>> $rows
*/
private function batchInsertIgnoreTags(array $rows): int
{
if ($rows === []) {
return 0;
}
$chunks = array_chunk($rows, 500);
$affected = 0;
foreach ($chunks as $chunk) {
$values = [];
$params = [];
foreach ($chunk as $r) {
$values[] = '(?,?,?,?,?,?,?,?)';
$params[] = $r['external_userid'];
$params[] = $r['follow_user_id'];
$params[] = $r['tag_id'];
$params[] = $r['tag_name'];
$params[] = $r['group_name'];
$params[] = $r['type'];
$params[] = $r['create_time'];
$params[] = $r['update_time'];
}
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
$sql = "INSERT IGNORE INTO {$prefix}qywx_external_contact_tag "
. '(external_userid, follow_user_id, tag_id, tag_name, group_name, type, create_time, update_time) VALUES '
. implode(',', $values);
Db::execute($sql, $params);
$affected += count($chunk);
}
return $affected;
}
/**
* 用 CASE WHEN id THEN val 一条 SQL 批量 UPDATE tags JSON。
*
* @param array<int, string> $idToTagsJson
*/
private function batchUpdateTagsJson(array $idToTagsJson): int
{
if ($idToTagsJson === []) {
return 0;
}
$chunks = array_chunk($idToTagsJson, 500, true);
$affected = 0;
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
foreach ($chunks as $chunk) {
$cases = [];
$ids = [];
$params = [];
foreach ($chunk as $id => $tagsJson) {
$cases[] = 'WHEN ? THEN ?';
$params[] = $id;
$params[] = $tagsJson;
$ids[] = (int) $id;
}
$idList = implode(',', $ids);
$sql = "UPDATE {$prefix}qywx_external_contact SET tags = CASE id "
. implode(' ', $cases)
. " END WHERE id IN ({$idList})";
Db::execute($sql, $params);
$affected += count($chunk);
}
return $affected;
}
}
+8 -2
View File
@@ -31,10 +31,16 @@ class SyncTrackingNumbers extends Command
$startTime = microtime(true);
try {
// 查询所有有快递单号的订单
// 终态订单不再触发查件:已完成(3)/已取消(4)/已签收(6)/暂不制药(8)/拒收(9)/退款(10)/保留药方(11)/制药缓发(12)
$terminalFulfillmentStatus = [3, 4, 6, 8, 9, 10, 11, 12];
// 查询所有有快递单号、未结案、且未上传甘草的订单
// 已上传甘草(gancao_reciperl_order_no 非空)的物流由甘草侧 GancaoLogisticsRouteService 拉取,不重复走快递100
$orders = Db::name('tcm_prescription_order')
->where('tracking_number', '<>', '')
->whereNull('delete_time')
->whereNotIn('fulfillment_status', $terminalFulfillmentStatus)
->whereRaw("TRIM(COALESCE(gancao_reciperl_order_no, '')) = ''")
->field([
'id',
'tracking_number',
@@ -51,7 +57,7 @@ class SyncTrackingNumbers extends Command
$skipped = 0;
$failed = 0;
$output->writeln("找到 {$total} 个有快递单号的订单");
$output->writeln("找到 {$total} 个有快递单号、未结案、未上传甘草的订单(已跳过已完成/已取消/已签收等终态及甘草已托管订单)");
foreach ($orders as $order) {
try {