From 0310045caa03f7cf96866c39537379c13dba63fd Mon Sep 17 00:00:00 2001 From: weizong song Date: Sat, 9 Aug 2025 11:19:07 +0800 Subject: [PATCH] up --- .../Commands/GenerateSupplyDemandDemo.php | 30 +++ .../SupplyDemandTestDataGenerator.php | 181 ++++++++++++++++++ database/seeders/SupplyDemandDemoSeeder.php | 19 ++ docs/supply_demand_module_analysis.md | 96 ++++++++++ storage/api-docs/api-docs.json | 0 5 files changed, 326 insertions(+) create mode 100644 app/Console/Commands/GenerateSupplyDemandDemo.php create mode 100644 app/Services/TestData/SupplyDemandTestDataGenerator.php create mode 100644 database/seeders/SupplyDemandDemoSeeder.php create mode 100644 docs/supply_demand_module_analysis.md mode change 100644 => 100755 storage/api-docs/api-docs.json diff --git a/app/Console/Commands/GenerateSupplyDemandDemo.php b/app/Console/Commands/GenerateSupplyDemandDemo.php new file mode 100644 index 0000000..646d8b3 --- /dev/null +++ b/app/Console/Commands/GenerateSupplyDemandDemo.php @@ -0,0 +1,30 @@ +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; + } +} + + diff --git a/app/Services/TestData/SupplyDemandTestDataGenerator.php b/app/Services/TestData/SupplyDemandTestDataGenerator.php new file mode 100644 index 0000000..2f00268 --- /dev/null +++ b/app/Services/TestData/SupplyDemandTestDataGenerator.php @@ -0,0 +1,181 @@ +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 + )); + } + }); + } +} + + diff --git a/database/seeders/SupplyDemandDemoSeeder.php b/database/seeders/SupplyDemandDemoSeeder.php new file mode 100644 index 0000000..334ce73 --- /dev/null +++ b/database/seeders/SupplyDemandDemoSeeder.php @@ -0,0 +1,19 @@ +generate(50, 20, function (string $msg) { + $this->command?->info($msg); + }); + } +} + + diff --git a/docs/supply_demand_module_analysis.md b/docs/supply_demand_module_analysis.md new file mode 100644 index 0000000..da55521 --- /dev/null +++ b/docs/supply_demand_module_analysis.md @@ -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。 + +- **方式B:Artisan 命令(Console Command)** + - 优点: + - 支持运行参数(如 `--count`、`--users`),灵活生成规模; + - 可多次按需执行,便于演示或局部补数; + - 缺点: + - 需要单独维护命令逻辑; + - 不会被自动纳入 `db:seed` 的全局流程。 + +推荐:开发/演示期使用命令(B)灵活试验;集成测试或初始化环境使用 Seeder(A)。 + + diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json old mode 100644 new mode 100755