weizong song 3 months ago
parent a6f8bc8dbc
commit 0310045caa

@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use App\Services\TestData\SupplyDemandTestDataGenerator;
use Illuminate\Console\Command;
class GenerateSupplyDemandDemo extends Command
{
protected $signature = 'demo:generate-supply-demand {--count=50} {--users=20}';
protected $description = '生成供需模块演示数据(含附件、会话、消息、收藏等)';
public function handle(): int
{
$count = (int)$this->option('count');
$users = (int)$this->option('users');
$this->info("开始生成:供需 {$count} 条,最少用户 {$users} 个...");
$generator = new SupplyDemandTestDataGenerator();
$generator->generate($count, $users, function (string $msg) {
$this->line($msg);
});
$this->info('生成完成');
return self::SUCCESS;
}
}

@ -0,0 +1,181 @@
<?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 = $faker->sentence(4);
$supplyDemand->type = $faker->randomElement([1, 2]);
$supplyDemand->content = $faker->paragraphs($faker->numberBetween(1, 3), true);
$supplyDemand->tag = implode(',', $faker->randomElements(['采购', '合作', '招聘', '渠道', '推广', '技术', '投资'], $faker->numberBetween(1, 3)));
$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
));
}
});
}
}

@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Services\TestData\SupplyDemandTestDataGenerator;
use Illuminate\Database\Seeder;
class SupplyDemandDemoSeeder extends Seeder
{
public function run(): void
{
$generator = new SupplyDemandTestDataGenerator();
$generator->generate(50, 20, function (string $msg) {
$this->command?->info($msg);
});
}
}

@ -0,0 +1,96 @@
### 供需发布模块数据结构与功能分析
#### 一、数据结构
- **表:`supply_demands`(供需主表)**
- 字段:`id`, `user_id`, `title`, `type`(1=供应,2=需求), `content`, `tag`, `wechat`, `mobile`, `email`, `status`(0待审核/1通过/2拒绝/3退回修改/4永久隐藏), `view_count`, `contact_count`, `expire_time`, `public_way`(布尔实际业务含义为1/2/3), `file_ids`(JSON), `contact_name`, `timestamps`, `deleted_at`
- 关系:
- `user`: 发布者(`hasOne User(id=user_id)`
- `keeps`: 收藏(`hasMany SupplyDemandKeep(supply_demand_id=id)`
- 虚拟属性:`files`(根据 `file_ids` 关联 `Upload` 列表)
- **表:`supply_demand_keeps`(收藏表)**
- 字段:`id`, `user_id`, `supply_demand_id`, `timestamps`, `deleted_at`
- 关系:
- `user`: 收藏者(`hasOne User(id=user_id)`
- `supplyDemand`: 被收藏的供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`dialogues`(会话表)**
- 字段:`id`, `user_id`, `to_user_id`, `supply_demand_id`, `last_content`, `last_datetime`, `timestamps`, `deleted_at`
- 关系:
- `user`: 会话发起方(`hasOne User(id=user_id)`
- `toUser`: 会话接收方(`hasOne User(id=to_user_id)`
- `supplyDemand`: 关联供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`messages`(消息表)**
- 字段:`id`, `dialogue_id`, `user_id`, `to_user_id`, `supply_demand_id`, `content`, `is_read`, `timestamps`, `deleted_at`
- 关系:
- `user`/`toUser`: 发送方/接收方(`hasOne User`
- `dialogue`: 所属会话(`hasOne Dialogue(id=dialogue_id)`
- `supplyDemand`: 关联供需(`hasOne SupplyDemand(id=supply_demand_id)`
- **表:`uploads`(附件表)**
- 字段:`id`, `belongs_type`, `belongs_id`, `original_name`, `folder`, `name`, `extension`, `size`, `creator_type`, `creator_id`, `timestamps`, `deleted_at`
- 用途:`supply_demands.file_ids` 存放附件 `id` 列表,模型通过 `files` 访问器取回 `Upload` 集合
说明:`public_way` 迁移中为 boolean但注释为 1/2/3 三种模式1直接公开/2私信后公开/3不公开。当前以 0/1 存储,若需完整三态应在后续迁移中改为 tinyInteger。
#### 二、功能说明(`SupplyDemandController`
- **列表 `index`**:按类型、状态、关键词、是否只看自己、有效期(有效/失效)筛选,分页排序;关联返回 `user` 基本信息。
- **详情 `detail`**:按 `id` 查询,返回 `user`,并自增 `view_count`;附带当前用户对该供需的已发私信次数。
- **保存 `save`**:新增或更新(新增时绑定当前用户并短信通知管理员),使用事务保存,支持 `file_ids`、`expire_time`、`contact_name` 等字段。
- **删除 `destroy`**:按 `id` 软删除。
- **发私信 `sendMessage`**
- 若带 `supply_demand_id` 则自增浏览量;
- 无会话则创建会话;
- 限流:每天发送条数不超过配置 `message_limit`
- 反骚扰:自己连续发送后需等待对方回复;
- 保存消息并更新会话最后内容与时间。
- **消息列表 `messageList`**:按 `to_user_id` 定位当前和对方的会话,分页返回消息(含双方用户信息)。
- **会话列表 `dialogues`**:返回与当前用户相关的会话(发起或接收),含双方用户与关联供需。
- **收藏相关**
- `keepIndex`:我的收藏列表;
- `keepSupplyDemand`:收藏,去重创建;
- `unKeepSupplyDemand`:取消收藏。
#### 三、核心业务要点
- 审核机制:多状态闭环(待审/通过/拒绝/退回/隐藏)。
- 私信策略:限流 + 反骚扰(需对方回复后再发)。
- 有效期:支持有效/失效筛选。
- 公开模式当前存储为布尔0/1业务含义为三态后续建议迁移调整。
- 数据统计:浏览数 `view_count`、联系数 `contact_count`(可按消息交互推导)。
- 附件:`file_ids` JSON + `files` 访问器联表读取。
#### 四、测试数据生成目标
`supply_demands` 为起点,为每条主记录自动生成:
- 合理的 `uploads` 附件0~3 个),`file_ids` 同步写入;
- 真实的会话 `dialogues`0~2 个),双方用户随机;
- 合法的消息序列 `messages`1~5 条),严格交替往来确保不违反“需对方回复”约束;
- 合理的收藏 `supply_demand_keeps`0~5 条,去重);
- 统计字段:`view_count` ≥ 消息条数,`contact_count` 依据消息往来推导。
#### 五、两种生成方式对比
- **方式A数据库填充Seeder**
- 优点:
- 一次性执行、可集成到 CI 或初始化流程;
- 可与 `DatabaseSeeder` 串联;
- 便于多环境批量重置数据。
- 缺点:
- 运行参数(数量、用户规模)固定或需改代码;
- 无交互,临时性需求需改代码或 env。
- **方式BArtisan 命令Console Command**
- 优点:
- 支持运行参数(如 `--count`、`--users`),灵活生成规模;
- 可多次按需执行,便于演示或局部补数;
- 缺点:
- 需要单独维护命令逻辑;
- 不会被自动纳入 `db:seed` 的全局流程。
推荐:开发/演示期使用命令B灵活试验集成测试或初始化环境使用 SeederA
Loading…
Cancel
Save