跳到主要内容

优尾惠微信朋友圈商城

阅读需 8 分钟

基于微信 iPad 协议深度开发的朋友圈智能数据聚合与电商化解决方案,实现微商内容自动化采集、结构化处理与多端同步分发

项目介绍

技术架构

  • 前端: 跨端统一开发体系

    • UniApp 多端编译:一套代码同时生成 H5 + 微信小程序 + 抖音小程序,确保多平台 UI/UX 一致性
    • 高性能渲染优化:针对朋友圈富媒体(图片/视频)进行懒加载与 CDN 加速,提升用户浏览体验
  • 后端:高并发微服务架构

    • 核心服务层:采用 Webman 框架(PHP),相比传统 Laravel 性能提升 300%+,同时保持优雅的开发范式
    • 异步任务引擎:基于协程实现高并发数据采集,按商户权重动态分配采集任务,确保 1 分钟内完成发圈→采集→上架全流程
    • 分布式文件处理:
    • 图片/视频下载任务队列化,结合 Event-Driven 架构异步通知处理结果
    • 支持断点续传与失败自动重试,保障数据完整性
  • AI 驱动的数据智能解析

    • 非结构化文本处理:通过 LLM(大语言模型) 精准提取商品标题、价格、库存等关键信息,结构化输出 CSV
    • 智能语义分析提示词:
      const chatProms = [
      "帮我分析一组标题,输出csv列表的格式,不要输出分析过程,不要解释分析过程,不要输出请注意这类回复信息,不要换行符",
      "要求1:分析标题如果是卖货类标题则继续分析出:ID,商品名称,数量,价格,英文标题",
      "要求2:忽略掉计算不出结果的标题",
      "要求3:用csv形式输出数据",
      "要求4:各元素排列顺序为:ID,商品名称,数量,价格",
      "要求5:数量和价格要为数字格式,如果不符合规范要求则输出0",
      " 要求6:翻译商品名称为英文标题",
      ];
    • 准确率 >95%,显著降低人工校验成本
  • 扩展服务:NestJS 实战落地

    • 公众号后端:采用 NestJS(Node.js) 构建模块化服务,通过本项目验证其企业级开发可行性
    • 技术选型心得:

    以真实项目驱动技术探索,在解决依赖注入微服务通信等复杂问题中快速积累架构经验,后续将作为主力技术栈推广。"

  • 技术亮点

    • ✅ 协议级数据采集:绕过公开 API 限制,直接通过 iPad 协议获取朋友圈原始数据
    • ✅ 万级 QPS 处理能力:协程 + 队列 + 事件驱动,轻松应对高并发场景
    • ✅ AI 增强型 pipeline:从杂乱文本到结构化商品数据,全流程自动化
    • ✅ 渐进式技术演进:在生产环境验证 Webman/NestJS 等前沿方案,平衡性能与开发效率

来几张截图

NKQ6dY


NKQ6dY


NKQ6dY NKQ6dY NKQ6dY

随便贴几段源码

<?php

namespace app\task;

use app\command\PyqGetFriendCircle;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Workerman\Crontab\Crontab;

class GetFriendsCircleTask extends TaskBase
{
public function onWorkerStart(): void
{
new Crontab('0 */1 * * * *', function () {
echo 'GetFriendsCircleTask::GetFriendsCircle => ' . date('Y-m-d H:i:s') . "\n";
outputLog("GetFriendsCircleTask::GetFriendsCircle => ", date('Y-m-d H:i:s'));
$this->execute([]);
});
}

public function execute($args): int
{
$pyqGetFriendCircle = new PyqGetFriendCircle();
$input = new ArrayInput(array());
$output = new BufferedOutput();
try {
$pyqGetFriendCircle->run($input, $output);
echo $output->fetch();
} catch (ExceptionInterface $e) {
outputLog("PyqGetFriendCircle step_exception => ", $e->getMessage());
echo "PyqGetFriendCircle step_exception => " . $e->getMessage() . "\n";
}
return 1;
}

public function task_desc(): string
{
return "获取朋友圈任务";
}
}

<?php

namespace app\api\logic;

use app\commons\CommonException;
use app\commons\helper\PushHelper;
use app\model\ChatHistory;
use app\service\BaseApiService;
use plugin\saiadmin\app\model\system\SystemUser;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\db\Query;

class ChatService extends BaseApiService
{
protected PushHelper $pushHelper;
protected MerchantService $merService;

public function __construct(PushHelper $pushHelper)
{
$this->model = new ChatHistory();
$this->pushHelper = $pushHelper;
$this->merService = new MerchantService();
}

/**
* @throws DbException
*/
public function getRecentChatHistory($sender_id, $receiver_id, $page = 1, $pageSize = 50): array
{
$where_sender = function (Query $query) use ($sender_id, $receiver_id) {
return $query->where('sender_id', $sender_id)
->where('receiver_id', $receiver_id);
};
$where_receiver = function (Query $query) use ($sender_id, $receiver_id) {
return $query->where('receiver_id', $sender_id)
->where('sender_id', $receiver_id);
};

// 分页查询
$chatHistory = ChatHistory::with([
'sender' => function (Query $query) {
$query->field('id,username,avatar');
},
'receiver' => function ($query) {
$query->field('id,username,avatar');
}
])
->where($where_sender)
->whereOr($where_receiver)
->order('send_time', 'desc')
->paginate([
'page' => 1,
'list_rows' => 100,
]);

$chatHistory = $chatHistory->map(function ($item) use ($sender_id, $receiver_id) {
$merService = new MerchantService();
$sender_user_id = $item['sender_id'];
$receiver_user_id = $item['receiver_id'];

$sendUser = SystemUser::field('id, username,nickname,avatar')->find($sender_user_id);
$recvUser = SystemUser::field('id, username,nickname,avatar')->find($receiver_user_id);

try {
$sendMerInfo = $merService->getMerchantFromUserId($sender_user_id);
$item['sender_nickname'] = $sendMerInfo['shop_name'];
$item['sender_username'] = $sendMerInfo['shop_name'];
$item['sender_avatar'] = $sendMerInfo['mer_logo'];
} catch (\Exception $e) {
$item['sender_nickname'] = $sendUser['nickname'];
$item['sender_username'] = $sendUser['username'];
$item['sender_avatar'] = $sendUser['avatar'];
}

try {
$recvMerInfo = $merService->getMerchantFromUserId($receiver_user_id);
$item['receiver_nickname'] = $recvMerInfo['shop_name'];
$item['receiver_username'] = $recvMerInfo['shop_name'];
$item['receiver_avatar'] = $recvMerInfo['mer_logo'];
} catch (\Exception $e) {
$item['receiver_nickname'] = $recvUser['nickname'];
$item['receiver_username'] = $recvUser['username'];
$item['receiver_avatar'] = $recvUser['avatar'];
}
return $item;
});

outputLog('chatHistory => ', $chatHistory->toArray());
return $chatHistory->reverse()->filter()->toArray(); // 返回分页数据
}

/**
* 获取与当前用户聊天过的会话列表
*
* @param int $userId 当前登录用户的ID
* @return array 返回会话列表
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public static function getChatSessions(int $userId, $page = 1, $pageSize = 10): array
{
// 子查询:获取每个会话的最后一条消息内容
$subQuery = ChatHistory::field('message_content')
->where(function ($query) use ($userId) { // 获取发送者为当前用户的会话
$query->where('sender_id', $userId)
->whereColumn('receiver_id', 'chat_user_id');
})
->whereOr(function ($query) use ($userId) { // 获取接收者为当前用户的会话
$query->where('receiver_id', $userId)
->whereColumn('sender_id', 'chat_user_id');
})
->order('send_time', 'desc')
->limit(1)
->buildSql();

// 主查询:获取会话列表
try {
$sessions = ChatHistory::field([
'CASE
WHEN sender_id = ' . $userId . ' THEN receiver_id
WHEN receiver_id = ' . $userId . ' THEN sender_id
END AS chat_user_id',
'MAX(send_time) AS last_message_time',
'(' . $subQuery . ') AS last_message_content',
])
->where(function (Query $query) use ($userId) {
$query->where('sender_id', $userId)
->whereOr('receiver_id', $userId);
})
->bind(['userId' => $userId])
->group('CASE
WHEN sender_id = ' . $userId . ' THEN receiver_id
WHEN receiver_id = ' . $userId . ' THEN sender_id
END'
) // 使用原始表达式进行分组
->order('last_message_time', 'desc')
->paginate($pageSize, false);

$sessions = $sessions->map(function ($item) use ($userId) {
$merService = new MerchantService();
$chat_user_id = $item['chat_user_id'];
$chatUser = SystemUser::field('id, username,avatar')->find($chat_user_id);
$item['avatar'] = $chatUser['avatar'];
$item['username'] = hidePhoneNumberSecure($chatUser['username']);
$item['chat_user_id'] = $chatUser['hashid'];
$item['is_mer'] = 0;
try {
$merInfo = $merService->getMerchantFromUserId($chat_user_id);
$item['username'] = $merInfo['shop_name'];
$item['avatar'] = $merInfo['mer_logo'];
$item['is_mer'] = 1;
} catch (\Exception $e) {
}

// 统计未读消息数量
$unreadCount = ChatHistory::where('receiver_id', $userId)
->where('sender_id', $chat_user_id)
->where('message_status', 'sent')
->count();

$item['unread_count'] = $unreadCount;
return $item;
});
} catch (DbException $e) {
return ['sessions' => $e->getData()];
}
return $sessions->toArray(); // 返回分页数据
}

/**
* @throws DbException
*/
public function deleteMsg($sender_id, $receiver_id): bool|int
{
return ChatHistory::where(function ($query) use ($receiver_id, $sender_id) {
$query->where('sender_id', $sender_id)
->where('receiver_id', $receiver_id);
})->whereOr(function ($query) use ($receiver_id, $sender_id) {
$query->where('sender_id', $receiver_id)
->where('receiver_id', $sender_id);
})->delete();
}

/**
* @throws DbException
*/
public function makeAsRead($sender_id, $receiver_id): int|ChatHistory|Query
{
return ChatHistory::where(function ($query) use ($sender_id, $receiver_id) {
$query->where('sender_id', $sender_id)
->where('receiver_id', $receiver_id);
})->whereOr(function ($query) use ($receiver_id, $sender_id) {
$query->where('sender_id', $receiver_id)
->where('receiver_id', $sender_id);
})->update(['message_status' => 'read']);
}

public function sendMsg($senderUser, $receiver_id, $message_content, $saveHistory = true): \think\Model|ChatHistory|array|bool
{
outputLog('sendMsg=>', [$senderUser, $receiver_id, $message_content]);
try {
if ($senderUser['id'] == $receiver_id) {
throw new CommonException('不能给自己发消息');
}
$this->pushHelper->pushUniappImMsg(
['id' => $senderUser['id'], 'nickname' => $senderUser['username'], 'avatar' => $senderUser['avatar']],
$receiver_id, $message_content
);
$data = [
'sender_id' => $senderUser['id'],
'receiver_id' => $receiver_id,
'message_content' => $message_content,
'send_time' => date('Y-m-d H:i:s'),
'message_status' => 'sent'
];
if ($saveHistory) {
return ChatHistory::create($data);
}
return $data;
} catch (\Exception $e) {
outputLog("sendMsg step_Exception", $e->getMessage());
return false;
}
}
}
<?php

namespace app\command;

use app\commons\api\ai\BaiduAi;
use app\commons\api\ai\Deepseek;
use app\commons\helper\BaiduAiHelper;
use app\model\Product;
use Carbon\Carbon;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use plugin\saiadmin\utils\Arr;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use think\Collection;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;


/*
商品上架
*/

class PyqProductUp extends Command
{
protected static string $defaultName = 'pyq:productUp';
protected static string $defaultDescription = 'pyq productUp';

const BatchSize = 8;

/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
commandOutput($output, 'pyq:productUp start', '商品上架');
try {
$this->handleProducts($output);
} catch (\Exception $e) {
commandOutput($output, 'pyq:productUp step_exception => ', $e->getMessage());
}
commandOutput($output, 'pyq:productUp end');
return self::SUCCESS;
}

/* 搜索待上架的商品,每次处理50个*/
/**
* @throws ModelNotFoundException
* @throws DataNotFoundException
* @throws DbException
*/
protected function handleProducts(OutputInterface $output): void
{
$hasMore = true;
while ($hasMore) {
commandOutput($output, 'pyq:handleProducts', 'loop---------------------');
$productList = Product::where('is_show', 0)
->whereNull('ai_parse_json')
->limit(self::BatchSize)->select();
$this->handleBatch($output, $productList);

$productCount = $productList->count();
$hasMore = $productCount >= self::BatchSize;

commandOutput($output, 'pyq:handleProducts', compact('hasMore', 'productCount'));

sleep(5);
}
commandOutput($output, 'pyq:handleProducts', 'loop_end---------------------');
}

protected function handleBatch(OutputInterface $output, Collection $productList): void
{
$logic = new SystemConfigLogic();
$batchSize = intval($logic->getConfig("aiBatchSize")['value']);
commandOutput($output,"batchSize0 =>",$batchSize);
$batchSize = $batchSize <= 0 ? self::BatchSize : $batchSize;
commandOutput($output,"batchSize1 =>",$batchSize);
if ($productList->count() < $batchSize) {
commandOutput($output, sprintf('待处理的商品数量%d不足%d条,待下次处理', $productList->count(), $batchSize));
return;
}

$title_prompts = [];
$batchProductIds = [];
foreach ($productList as $product) {
$batchProductIds[] = $product->id;
$title_prompts[] = sprintf("ID:%d,%s", $product->id, $product->goods_name);
}
commandOutput($output, '本次要处理的标题 => ', $title_prompts);

$ai = new BaiduAi();
// $ai = new Deepseek();
$res = $ai->titleChat($title_prompts);

commandOutput($output, 'step_titleChat_res => ', $res);

$parseResults = $ai->parseResult($res);
commandOutput($output, '本次分析结果 => ', $parseResults);

$upProductIds = [];
foreach ($parseResults as $item) {
$productId = $item['id'];
$upProductIds[] = $productId;
try {
$data = [
'ai_parse_json' => $item,
'is_show' => 2,
'price' => $item['price'] ?? 0,
'stock' => $item['count'] ?? 0,
'english_title' => $item['english_title'] ?? '',
'auto_on_time' => Carbon::now()->toDateTimeString()
];
$upRes = Product::where('id', $productId)->update($data);
// commandOutput($output, "PyqProductUp::step_updateRes => ", $upRes);
} catch (DbException $e) {
commandOutput($output, "PyqProductUp::step_updateProduct exception => ", $e->getMessage());
}
// commandOutput($output, "PyqProductUp::step_item end => ", $item);
}

// 比较$batchProductIds和$upProductIds差集,没有处理的,更新为ai处理失败
$diffProductIds = array_diff($batchProductIds, $upProductIds);
if (!empty($diffProductIds)) {
commandOutput($output, 'PyqProductUp::step_ai分析失败的商品id列表 => ', $diffProductIds);
Product::whereIn('id', $diffProductIds)->update([
'ai_parse_json' => ['is_show' => 1],
'is_show' => 1, //自动上架失败
]);
}
}

/**
* @throws DbException
*/
public function test(OutputInterface $output): void
{
$parseResultStr = <<<EOT
[
{
"id": "2",
"name": "茶灵(CHALING) 香氛",
"count": "100ml",
"price": ""
},
{
"id": "10",
"name": "",
"count": "",
"price": ""
},
]
EOT;

$parseResults = json_decode($parseResultStr, true);
foreach ($parseResults as $item) {
$productId = $item['id'];
commandOutput($output, '本次分析Item => ', $item);
$updateRes = Product::where('id', $productId)->update([
'ai_parse_json' => $item,
'is_show' => 0,
'price' => is_numeric($item['price']) ? $item['price'] : 0,
'stock' => is_numeric($item['count']) ? $item['price'] : 0,
]);

commandOutput($output, '本次分析Item res => ', $updateRes);
}
}
}
Loading Comments...