fail([ResponseCode::ERROR_BUSINESS, $text]); } private function baseQuery() { $q = SupplyDemand::with('user'); foreach (request('filter', []) as $f) { if (!in_array($f['key'] ?? '', ['title','status','type','created_at'], true)) continue; if (($f['op'] ?? '') === 'eq') $q->where($f['key'], $f['value']); elseif (($f['op'] ?? '') === 'like') $q->where($f['key'], 'like', '%' . $f['value'] . '%'); } if (request('binding') === 'unbound') $q->where('is_proxy', 1)->where('user_id', 0); if (request('binding') === 'bound') $q->where('user_id', '>', 0); return $q; } public function index() { $q = $this->baseQuery()->orderBy('id','desc'); if (request('is_export')) return $this->exportDemands($q); $list = $q->paginate(min(100, max(1, (int) request('page_size',20)))); $list->getCollection()->transform(function ($d) { return Profiles::demand($d, true); }); $out = $list->toArray(); $out['pending_count'] = SupplyDemand::whereIn('status',[0,3])->count(); return $this->success($out); } public function show() { $d = SupplyDemand::find(request('id')); return $d ? $this->success(Profiles::demand($d, true)) : $this->error('数据不存在'); } public function save() { $d = request('id') ? SupplyDemand::find(request('id')) : new SupplyDemand(); if (!$d) return $this->error('数据不存在'); $data = request()->only(['title','type','content','tag','wechat','mobile','email','expire_time','public_way','file_ids','contact_name','fund_type','amount','fund_stage','fund_company','industry_type','product','desc','status']); $v = Validator::make($data, ['title'=>($d->exists ? 'sometimes|' : 'required|').'string|max:100','type'=>'sometimes|integer|in:1,2,3','status'=>'sometimes|integer|in:0,1,2,3,4','content'=>'nullable|string|max:10000','expire_time'=>'nullable|date']); if ($v->fails()) return $this->error(implode(';',$v->errors()->all())); if (!$d->exists || $d->is_proxy) { if (!$d->exists || !$d->user_id) { $proxy = request()->only(['publisher_name','publisher_company','publisher_mobile']); $pv = Validator::make($proxy, ['publisher_name'=>'required|string|max:100','publisher_company'=>'required|string|max:255','publisher_mobile'=>['required','regex:/^1\d{10}$/']]); // 审核按钮只更新状态,无需重新传发布人信息。 if (!$d->exists || count($proxy)) { if ($pv->fails()) return $this->error('请填写发布人姓名、公司及有效手机号'); $data = array_merge($data, $proxy); } } $data['public_way'] = 3; if (!$d->exists) $data = array_merge($data, ['is_proxy'=>1,'user_id'=>0,'status'=>0,'type'=>$data['type'] ?? 2,'created_by_admin_id'=>$this->getUserId()]); } $d->fill($data); $d->save(); if ($d->is_proxy && !$d->user_id) { $user = User::where('mobile',$d->publisher_mobile)->whereNotNull('supply_mobile_verified_at')->first(); if ($user) Handover::autoBind($user); } return $this->success(Profiles::demand($d->fresh(), true)); } public function destroy() { $d = SupplyDemand::find(request('id')); if (!$d || !$d->is_proxy) return $this->error('仅可删除后台代发需求'); $d->delete(); return $this->success('删除成功,历史消息已保留'); } public function batchPublish() { $ids = request('ids', []); $v = Validator::make(['ids'=>$ids], ['ids'=>'required|array|min:1|max:1000','ids.*'=>'required|integer|distinct']); if ($v->fails()) return $this->error('请选择需要发布的需求'); try { $count = DB::transaction(function () use ($ids) { $rows = SupplyDemand::whereIn('id',$ids)->lockForUpdate()->get(); if ($rows->count() !== count($ids)) throw new \InvalidArgumentException('部分需求已不存在,请刷新'); foreach ($rows as $d) { if (!$d->title || (!$d->content && !$d->desc)) throw new \InvalidArgumentException('存在标题或内容不完整的需求,请先检查'); if ($d->expire_time && Carbon::parse($d->expire_time)->isPast()) throw new \InvalidArgumentException('存在已过期需求,请先修改有效期'); $d->status=1; $d->save(); } return $rows->count(); }); return $this->success(['count'=>$count]); } catch (\InvalidArgumentException $e) { return $this->error($e->getMessage()); } } public function importPreview() { $v = Validator::make(request()->all(), ['file'=>'required|file|mimes:xlsx,xls|max:5120']); if ($v->fails()) return $this->error('请上传不超过5MB的Excel文件'); try { $sheet = \PhpOffice\PhpSpreadsheet\IOFactory::load(request()->file('file')->getRealPath())->getActiveSheet(); if ($sheet->getHighestDataRow() > 1001) return $this->error('每次最多导入1000条'); $rows = $sheet->toArray(null, false, false, false); $header = array_shift($rows); if (array_slice($header,0,7) !== ImportRows::HEADERS) return $this->error('模板表头不符,请下载模板后填写'); $keys=['title','content','tag','publisher_name','publisher_company','publisher_mobile','expire_time']; $input=[]; foreach ($rows as $r) if (count(array_filter($r, function ($x) { return $x !== null && $x !== ''; }))) $input[]=array_combine($keys,array_slice(array_pad($r,7,''),0,7)); return $this->success(ImportRows::validate($input)); } catch (\Throwable $e) { return $this->error('无法读取文件,请检查模板和日期格式'); } } public function importRows() { try { $input = request('rows', []); if (!is_array($input)) return $this->error('导入格式错误'); $result = ImportRows::validate($input); if ($result['errors']) return $this->success($result + ['imported'=>0]); DB::transaction(function () use ($result) { foreach ($result['rows'] as $r) SupplyDemand::create($r + ['is_proxy'=>1,'user_id'=>0,'type'=>2,'status'=>0,'public_way'=>3,'created_by_admin_id'=>$this->getUserId()]); }); foreach (array_unique(array_column($result['rows'],'publisher_mobile')) as $mobile) { $u=User::where('mobile',$mobile)->whereNotNull('supply_mobile_verified_at')->first(); if ($u) Handover::autoBind($u); } return $this->success(['imported'=>count($result['rows']), 'errors'=>[]]); } catch (\InvalidArgumentException $e) { return $this->error($e->getMessage()); } catch (\Illuminate\Database\QueryException $e) { return $this->error('存在重复导入,请刷新预览后重试'); } } public function importTemplate() { return Excel::download(new SupplyDemandExport([], ImportRows::HEADERS), '需求导入模板.xlsx'); } public function bindUser() { if (Validator::make(request()->all(), ['id'=>'required|integer','user_id'=>'required|integer'])->fails()) return $this->error('请选择有效需求和学员'); $d=SupplyDemand::find(request('id')); $u=User::find(request('user_id')); if (!$d || !$u) return $this->error('需求或学员不存在'); try { return $this->success(Profiles::demand(Handover::bind($d,$u),true)); } catch (\InvalidArgumentException $e) { return $this->error($e->getMessage()); } } public function bindingUsers() { $keyword=trim((string) request('keyword')); if (mb_strlen($keyword)<2) return $this->success([]); $users=User::whereNotNull('supply_mobile_verified_at')->where(function ($q) use ($keyword) { $q->where('name','like','%'.$keyword.'%')->orWhere('mobile','like','%'.$keyword.'%'); })->limit(30)->get(); return $this->success($users->map(function($u) { return Profiles::user($u,true); })); } public function settings() { if (request()->isMethod('post')) { $data=request()->only(array_keys(Settings::DEFAULTS)); $v=Validator::make($data,['detail_tip'=>'required|string|max:500','chat_tip'=>'required|string|max:500','sent_tip'=>'required|string|max:200','sms_enabled'=>'required|in:0,1','sms_interval_minutes'=>'required|integer|min:1|max:1440','sms_daily_limit'=>'required|integer|min:1|max:100']); if ($v->fails()) return $this->error(implode(';',$v->errors()->all())); DB::transaction(function() use($data) { foreach($data as $k=>$value) Config::updateOrCreate(['key'=>'supply_demand_'.$k],['value'=>(string)$value]); }); } return $this->success(Settings::all()); } public function smsStatus() { return $this->success(DB::table('supply_demand_sms_notices')->orderBy('updated_at','desc')->paginate(20)); } public function retrySms() { DB::table('supply_demand_sms_notices')->where('user_id',request('user_id'))->whereNotNull('pending_at')->update(['attempts'=>0,'last_error'=>null]); return $this->success('已加入重试,仍遵循发送频率和每日上限'); } private function period() { $start=Carbon::parse(request('start_date',date('Y-m-d')))->startOfDay(); $end=Carbon::parse(request('end_date',date('Y-m-d')))->endOfDay(); if ($start->gt($end)) throw new \InvalidArgumentException('开始日期不能晚于结束日期'); return [$start,$end]; } private function periodMessages($start,$end) { return Message::whereBetween('created_at',[$start,$end])->whereColumn('user_id','<>','to_user_id')->whereHas('supplyDemand',function($q) { if (request('type')) $q->where('type',request('type')); if (request('binding')==='unbound') $q->where('user_id',0)->where('is_proxy',1); if (request('binding')==='bound') $q->where('user_id','>',0); }); } private function publicationQuery() { $q=SupplyDemand::query(); if (request('type')) $q->where('type',request('type')); if (request('binding')==='unbound') $q->where('user_id',0)->where('is_proxy',1); if (request('binding')==='bound') $q->where('user_id','>',0); return $q; } public function chart() { try { [$start,$end]=$this->period(); } catch (\Throwable $e) { return $this->error('请选择有效时间范围'); } $days=$start->diffInDays($end->copy()->startOfDay())+1; $prevEnd=$start->copy()->subDay()->endOfDay(); $prevStart=$start->copy()->subDays($days); $counts=function($from,$to) { $m=$this->periodMessages($from,$to); return [$this->publicationQuery()->whereBetween('created_at',[$from,$to])->count(),(clone $m)->count(), (clone $m)->whereNotNull('dialogue_id')->select('supply_demand_id','dialogue_id')->groupBy('supply_demand_id','dialogue_id')->havingRaw('COUNT(DISTINCT user_id)>=2')->get()->count()]; }; $now=$counts($start,$end); $prev=$counts($prevStart,$prevEnd); $list=$this->publicationQuery()->with(['user','messages'=>function($q) use($start,$end) { $q->whereBetween('created_at',[$start,$end])->with(['dialogue.user','dialogue.toUser'])->orderBy('id','desc'); }]) ->where(function($q) use($start,$end) { $q->whereBetween('created_at',[$start,$end])->orWhereHas('messages',function($m) use($start,$end) { $m->whereBetween('created_at',[$start,$end]); }); }) ->orderBy('id','desc')->paginate(min(100,max(1,(int)request('page_size',20)))); $list->getCollection()->transform(function($d) { $dialogues=$d->messages->pluck('dialogue')->filter()->unique('id')->values()->map(function($c) { return Conversations::summary($c,0,true); }); $d->unsetRelation('messages'); $out=Profiles::demand($d,true); $out['dialogues']=$dialogues; return $out; }); $out=['list'=>$list]; foreach(['supply_demand','message','interaction'] as $i=>$key) { $out[$key.'_count']=$now[$i]; $out['prev_'.$key.'_count']=$prev[$i]; $out[$key.'_growth_rate']=['rate'=>$prev[$i] ? round(($now[$i]-$prev[$i])/$prev[$i]*100,2) : ($now[$i]?100:0)]; } return $this->success($out); } public function messageList() { $q=Message::with(['user','toUser'])->where('supply_demand_id',request('supply_demand_id')); if (request('dialogue_id')) $q->where('dialogue_id',request('dialogue_id')); else { $a=request('user_id');$b=request('to_user_id'); $q->where(function($x) use($a,$b) { $x->where(function($z) use($a,$b) {$z->where('user_id',$a)->where('to_user_id',$b);})->orWhere(function($z) use($a,$b) {$z->where('user_id',$b)->where('to_user_id',$a);}); }); } $list=$q->orderBy('id','desc')->paginate(100); $list->getCollection()->transform(function($m) { return ['id'=>$m->id,'content'=>$m->content,'created_at'=>$m->created_at->format('Y-m-d H:i:s'),'user'=>Profiles::user($m->user,true),'to_user'=>Profiles::user($m->toUser,true)]; }); return $this->success(['message'=>$list]); } private function exportDemands($q) { if ((clone $q)->count()>5000) return $this->error('超过5000条,请缩小筛选范围'); $rows=[]; foreach($q->get() as $d) { $p=Profiles::publisher($d,true);$rows[]=[$d->id,$d->title,$d->content,$d->created_at,$d->user_id?'已绑定':'未绑定',$p['name'],$p['company_name'],$p['mobile']??'',$p['course_name'],$p['class_name']]; } return Excel::download(new SupplyDemandExport($rows,['需求编号','标题','内容','发布时间','绑定状态','发布人姓名','发布人公司','发布人联系方式','发布人报名课程','发布人所在班级']),'供需信息.xlsx'); } public function exportInteractions() { try { [$start,$end]=$this->period(); } catch (\Throwable $e) { return $this->error('请选择有效时间范围'); } $groups=$this->periodMessages($start,$end)->select('supply_demand_id','dialogue_id')->groupBy('supply_demand_id','dialogue_id')->limit(5001)->get(); if ($groups->count()>5000) return $this->error('超过5000条,请缩小时间范围'); $rows=[]; foreach($groups as $g) { $d=SupplyDemand::find($g->supply_demand_id); $c=Dialogue::find($g->dialogue_id); if(!$d || !$c) continue; $publisher=Profiles::publisher($d,true); $other=(int)$c->user_id===(int)$d->user_id ? $c->toUser : $c->user; $p=Profiles::user($other,true); if(!$p) continue; $messages=$this->periodMessages($start,$end)->where('supply_demand_id',$d->id)->where('dialogue_id',$c->id); $rows[]=[$d->id,$d->title,$d->content,$d->created_at,$d->user_id?'已绑定':'未绑定', $publisher['name'],$publisher['company_name'],$publisher['mobile']??'',$publisher['course_name'],$publisher['class_name'], $p['name'],$p['company_name'],$p['mobile']??'',$p['course_name'],$p['class_name'], (clone $messages)->min('created_at'),(clone $messages)->max('created_at'),$d->user_id && (clone $messages)->where('user_id',$d->user_id)->exists()?'已回复':'未回复']; } return Excel::download(new SupplyDemandExport($rows,['需求编号','需求标题','需求内容','发布时间','绑定状态','发布人姓名','发布人公司','发布人联系方式','发布人报名课程','发布人所在班级','对接人姓名','对接人公司','对接人联系方式','对接人报名课程','对接人所在班级','首次对接时间','最近对接时间','回复状态']),'供需交互统计.xlsx'); } }