Merge branch 'master' of ssh://47.101.48.251:/data/git/wx.sstbc.com

master
cody 4 months ago
commit cf7b13b699

@ -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,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;
}
}

@ -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