Published on

优尾惠微信朋友圈商城

Authors
  • Name
    Twitter

基于微信 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);
        }
    }
}