|
|
|
|
@ -0,0 +1,266 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\TestData;
|
|
|
|
|
|
|
|
|
|
use App\Models\Dialogue;
|
|
|
|
|
use App\Models\Message;
|
|
|
|
|
use App\Models\SupplyDemand;
|
|
|
|
|
use App\Models\SupplyDemandKeep;
|
|
|
|
|
use App\Models\Upload;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Faker\Factory as FakerFactory;
|
|
|
|
|
use Illuminate\Support\Arr;
|
|
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 供需模块测试数据生成器
|
|
|
|
|
*/
|
|
|
|
|
class SupplyDemandTestDataGenerator
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 生成供需相关的全量测试数据
|
|
|
|
|
*
|
|
|
|
|
* @param int $supplyDemandCount 生成供需数量
|
|
|
|
|
* @param int $minUsers 最少用户数量,不足会自动补充
|
|
|
|
|
* @param callable|null $logger 可选日志输出函数 function(string $message): void
|
|
|
|
|
*/
|
|
|
|
|
public function generate(int $supplyDemandCount = 50, int $minUsers = 20, ?callable $logger = null): void
|
|
|
|
|
{
|
|
|
|
|
$log = $logger ?? static function (string $message): void {
|
|
|
|
|
// no-op
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$faker = FakerFactory::create('zh_CN');
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($supplyDemandCount, $minUsers, $faker, $log) {
|
|
|
|
|
// 1) 确保用户数量
|
|
|
|
|
$currentUserCount = User::count();
|
|
|
|
|
if ($currentUserCount < $minUsers) {
|
|
|
|
|
$need = $minUsers - $currentUserCount;
|
|
|
|
|
$log("创建用户: {$need}");
|
|
|
|
|
// 使用 factory 填充用户
|
|
|
|
|
\App\Models\User::factory($need)->create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$allUsers = User::query()->get(['id', 'name', 'nickname', 'username']);
|
|
|
|
|
if ($allUsers->count() < 2) {
|
|
|
|
|
throw new \RuntimeException('生成对话与消息至少需要2个用户');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2) 生成供需主数据
|
|
|
|
|
for ($i = 0; $i < $supplyDemandCount; $i++) {
|
|
|
|
|
$publisher = $allUsers->random();
|
|
|
|
|
|
|
|
|
|
$supplyDemand = new SupplyDemand();
|
|
|
|
|
$supplyDemand->user_id = $publisher->id;
|
|
|
|
|
$supplyDemand->title = $this->generateTechBusinessTitle($faker);
|
|
|
|
|
$supplyDemand->type = $faker->randomElement([1, 2]);
|
|
|
|
|
$supplyDemand->content = $this->generateContentFromTitle($supplyDemand->title, $supplyDemand->type, $faker);
|
|
|
|
|
$supplyDemand->tag = implode(',', $this->generateTagsFromTitle($supplyDemand->title, $faker));
|
|
|
|
|
$supplyDemand->wechat = 'wx_' . $faker->bothify('??####');
|
|
|
|
|
$supplyDemand->mobile = $faker->phoneNumber();
|
|
|
|
|
$supplyDemand->email = $faker->safeEmail();
|
|
|
|
|
$supplyDemand->status = $faker->randomElement([0, 1, 2, 3, 4]);
|
|
|
|
|
$supplyDemand->view_count = 0; // 稍后回填
|
|
|
|
|
$supplyDemand->contact_count = 0; // 稍后回填
|
|
|
|
|
$supplyDemand->expire_time = $faker->optional(0.6)->dateTimeBetween('now', '+90 days')?->format('Y-m-d H:i:s');
|
|
|
|
|
// public_way 实际业务需要三态(1/2/3),但列类型为 boolean,这里按 0/1 赋值
|
|
|
|
|
$supplyDemand->public_way = $faker->boolean ? 1 : 0;
|
|
|
|
|
$supplyDemand->contact_name = $faker->name();
|
|
|
|
|
$supplyDemand->save();
|
|
|
|
|
|
|
|
|
|
// 3) 附件:0~3 个,并同步回填 file_ids
|
|
|
|
|
$uploadIds = [];
|
|
|
|
|
$attachmentsCount = $faker->numberBetween(0, 3);
|
|
|
|
|
for ($k = 0; $k < $attachmentsCount; $k++) {
|
|
|
|
|
$originalName = $faker->lexify('file_????') . '.' . $faker->randomElement(['pdf', 'png', 'jpg', 'docx']);
|
|
|
|
|
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
|
|
|
|
$upload = new Upload();
|
|
|
|
|
$upload->belongs_type = SupplyDemand::class;
|
|
|
|
|
$upload->belongs_id = $supplyDemand->id;
|
|
|
|
|
$upload->original_name = $originalName;
|
|
|
|
|
$upload->folder = 'test/' . date('Ymd');
|
|
|
|
|
$upload->name = $faker->uuid . '.' . $extension;
|
|
|
|
|
$upload->extension = $extension;
|
|
|
|
|
$upload->size = $faker->numberBetween(5 * 1024, 2 * 1024 * 1024);
|
|
|
|
|
$upload->creator_type = 'seeder';
|
|
|
|
|
$upload->creator_id = $publisher->id;
|
|
|
|
|
$upload->save();
|
|
|
|
|
$uploadIds[] = $upload->id;
|
|
|
|
|
}
|
|
|
|
|
if (!empty($uploadIds)) {
|
|
|
|
|
$supplyDemand->file_ids = $uploadIds;
|
|
|
|
|
$supplyDemand->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4) 会话与消息:0~2 个会话,每个会话 1~5 条消息,严格交替
|
|
|
|
|
$dialogueCount = $faker->numberBetween(0, 2);
|
|
|
|
|
$totalMessages = 0;
|
|
|
|
|
$totalContacts = 0; // 统计对发布者的有效联系次数
|
|
|
|
|
|
|
|
|
|
for ($d = 0; $d < $dialogueCount; $d++) {
|
|
|
|
|
// 选择一个不同于发布者的用户作为对话方
|
|
|
|
|
$other = $allUsers->where('id', '!=', $publisher->id)->random();
|
|
|
|
|
|
|
|
|
|
$dialogue = new Dialogue();
|
|
|
|
|
$dialogue->user_id = $faker->randomElement([$publisher->id, $other->id]); // 对话发起方随机
|
|
|
|
|
$dialogue->to_user_id = ($dialogue->user_id === $publisher->id) ? $other->id : $publisher->id;
|
|
|
|
|
$dialogue->supply_demand_id = $supplyDemand->id;
|
|
|
|
|
$dialogue->last_content = null;
|
|
|
|
|
$dialogue->last_datetime = null;
|
|
|
|
|
$dialogue->save();
|
|
|
|
|
|
|
|
|
|
$messageCount = $faker->numberBetween(1, 5);
|
|
|
|
|
$sender = $faker->randomElement([$publisher->id, $other->id]);
|
|
|
|
|
$receiver = ($sender === $publisher->id) ? $other->id : $publisher->id;
|
|
|
|
|
|
|
|
|
|
$lastContent = null;
|
|
|
|
|
$lastDatetime = null;
|
|
|
|
|
for ($m = 0; $m < $messageCount; $m++) {
|
|
|
|
|
$content = $faker->realText($faker->numberBetween(20, 80));
|
|
|
|
|
$createdAt = Carbon::now()->subDays($faker->numberBetween(0, 10))->addMinutes($faker->numberBetween(0, 1440));
|
|
|
|
|
|
|
|
|
|
$msg = new Message();
|
|
|
|
|
$msg->dialogue_id = $dialogue->id;
|
|
|
|
|
$msg->user_id = $sender;
|
|
|
|
|
$msg->to_user_id = $receiver;
|
|
|
|
|
$msg->supply_demand_id = $supplyDemand->id;
|
|
|
|
|
$msg->content = $content;
|
|
|
|
|
$msg->is_read = $faker->boolean ? 1 : 0;
|
|
|
|
|
$msg->created_at = $createdAt;
|
|
|
|
|
$msg->updated_at = $createdAt;
|
|
|
|
|
$msg->save();
|
|
|
|
|
|
|
|
|
|
// 统计“对发布者的联系”
|
|
|
|
|
if ($receiver === $publisher->id) {
|
|
|
|
|
$totalContacts++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lastContent = $content;
|
|
|
|
|
$lastDatetime = $createdAt->format('Y-m-d H:i:s');
|
|
|
|
|
|
|
|
|
|
// 严格交替:交换 sender/receiver
|
|
|
|
|
[$sender, $receiver] = [$receiver, $sender];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dialogue->last_content = $lastContent;
|
|
|
|
|
$dialogue->last_datetime = $lastDatetime;
|
|
|
|
|
$dialogue->save();
|
|
|
|
|
|
|
|
|
|
$totalMessages += $messageCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5) 收藏:0~5 个
|
|
|
|
|
$keepCount = $faker->numberBetween(0, 5);
|
|
|
|
|
if ($keepCount > 0) {
|
|
|
|
|
$keeperPool = $allUsers->where('id', '!=', $publisher->id)->pluck('id')->all();
|
|
|
|
|
$keepers = Arr::random($keeperPool, min($keepCount, count($keeperPool)));
|
|
|
|
|
foreach ((array)$keepers as $keeperId) {
|
|
|
|
|
SupplyDemandKeep::firstOrCreate([
|
|
|
|
|
'user_id' => $keeperId,
|
|
|
|
|
'supply_demand_id' => $supplyDemand->id,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6) 回填统计:浏览量 >= 消息数,联系次数 = 对发布者的消息次数
|
|
|
|
|
$supplyDemand->contact_count = $totalContacts;
|
|
|
|
|
$supplyDemand->view_count = $totalMessages + $faker->numberBetween(0, 20);
|
|
|
|
|
$supplyDemand->save();
|
|
|
|
|
|
|
|
|
|
$log(sprintf('供需#%d 已生成:附件%s个,会话%s个,消息%s条,收藏%s个',
|
|
|
|
|
$supplyDemand->id,
|
|
|
|
|
count($uploadIds),
|
|
|
|
|
$dialogueCount,
|
|
|
|
|
$totalMessages,
|
|
|
|
|
$keepCount
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成科技/商业取向的标题
|
|
|
|
|
*/
|
|
|
|
|
private function generateTechBusinessTitle($faker): string
|
|
|
|
|
{
|
|
|
|
|
$regions = ['华东', '华南', '华北', '西南', '中原', '长三角', '珠三角'];
|
|
|
|
|
$industries = ['制造', '零售', '医药', '教育', '金融', '能源', '物流', '政企'];
|
|
|
|
|
$materials = ['光刻胶', '硅片', 'CMP 抛光液', '陶瓷基板'];
|
|
|
|
|
$domains = ['电商推荐', '客服质检', '金融风控', '工业检测', '内容审核'];
|
|
|
|
|
$countries = ['新加坡', '印度尼西亚', '阿联酋', '沙特', '巴西', '墨西哥'];
|
|
|
|
|
|
|
|
|
|
$templates = [
|
|
|
|
|
'SaaS 渠道分销伙伴招募(' . $faker->randomElement($regions) . ')',
|
|
|
|
|
'云计算成本优化服务对接(' . $faker->randomElement($industries) . '行业)',
|
|
|
|
|
'AI 数据标注外包合作(' . $faker->randomElement($domains) . ')',
|
|
|
|
|
'跨境电商供应链合作(' . $faker->randomElement($countries) . '仓)',
|
|
|
|
|
'半导体材料采购需求(' . $faker->randomElement($materials) . ')',
|
|
|
|
|
'新能源充电桩 OEM/ODM 代工合作',
|
|
|
|
|
'企业私有化部署 DevOps 顾问服务',
|
|
|
|
|
'工业物联网传感器批量采购',
|
|
|
|
|
'大模型微调服务(' . $faker->randomElement($domains) . ')',
|
|
|
|
|
'移动端 SDK 联合推广与结算',
|
|
|
|
|
'本地化运营团队招募(' . $faker->randomElement($regions) . ')',
|
|
|
|
|
'出海广告投放合作(' . $faker->randomElement($countries) . ')',
|
|
|
|
|
'数据中台建设项目外包',
|
|
|
|
|
'企业安全渗透测试服务对接',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return $faker->randomElement($templates);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 基于标题生成结构化内容,围绕标题展开
|
|
|
|
|
* @param int $type 1=供应 2=需求
|
|
|
|
|
*/
|
|
|
|
|
private function generateContentFromTitle(string $title, int $type, $faker): string
|
|
|
|
|
{
|
|
|
|
|
$timeframeWeeks = $faker->numberBetween(2, 12);
|
|
|
|
|
$budget = $faker->randomElement(['5万-10万', '10万-30万', '30万-80万', '80万以上', '按效果结算']);
|
|
|
|
|
$scale = $faker->randomElement(['小规模试点', '区域级铺开', '全国推广', '跨境协同']);
|
|
|
|
|
$partner = $faker->randomElement(['渠道商', 'ISV', '系统集成商', '服务外包商', '硬件厂商', '联合营销伙伴']);
|
|
|
|
|
$kpi = $faker->randomElement(['留存率', '转化率', '交付周期', '单点成本', '渠道覆盖']);
|
|
|
|
|
|
|
|
|
|
$roleLine = $type === 1
|
|
|
|
|
? '供给能力:我们可提供成熟方案/产品/产能,支持灵活对接与深度合作。'
|
|
|
|
|
: '需求说明:我们需要优质方案/资源/产能,期待高效、稳定的交付能力。';
|
|
|
|
|
|
|
|
|
|
$sections = [
|
|
|
|
|
"【项目标题】{$title}",
|
|
|
|
|
'项目背景:' . $faker->realText($faker->numberBetween(40, 80)),
|
|
|
|
|
$roleLine,
|
|
|
|
|
'合作模式:' . $faker->randomElement(['佣金', '代理', '分销', '联合投放', '项目外包', '里程碑结算']),
|
|
|
|
|
"目标指标:重点关注{$kpi},预计{$timeframeWeeks}周达到{$scale}阶段性目标。",
|
|
|
|
|
"预算与周期:预算 {$budget},计划周期 {$timeframeWeeks} 周。",
|
|
|
|
|
"适配伙伴:优先 {$partner},具备行业交付经验者加分。",
|
|
|
|
|
'补充信息:' . $faker->realText($faker->numberBetween(40, 80)),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return implode("\n", $sections);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据标题提取/生成标签
|
|
|
|
|
* @return array<string>
|
|
|
|
|
*/
|
|
|
|
|
private function generateTagsFromTitle(string $title, $faker): array
|
|
|
|
|
{
|
|
|
|
|
$dictionary = ['AI', 'SaaS', '云计算', '大模型', '供应链', '半导体', '新能源', '跨境电商', '营销', '渠道', 'OEM', 'ODM', '物联网', '安全', '出海', '数据中台', 'DevOps'];
|
|
|
|
|
$matched = [];
|
|
|
|
|
foreach ($dictionary as $keyword) {
|
|
|
|
|
if (mb_stripos($title, $keyword) !== false) {
|
|
|
|
|
$matched[] = $keyword;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$extra = $faker->randomElements($dictionary, $faker->numberBetween(0, 2));
|
|
|
|
|
$tags = array_values(array_unique(array_merge($matched, $extra)));
|
|
|
|
|
if (empty($tags)) {
|
|
|
|
|
$tags = $faker->randomElements($dictionary, $faker->numberBetween(1, 3));
|
|
|
|
|
}
|
|
|
|
|
return $tags;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|