You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\Admin;
use App\Models\SupplyDemand;
use App\Models\User;
use App\Models\Message;
use App\Models\Dialogue;
use App\Models\Config;
use App\Services\SupplyDemand\Profiles;
use App\Services\SupplyDemand\ImportRows;
use App\Services\SupplyDemand\Settings;
use App\Services\SupplyDemand\Handover;
use App\Services\SupplyDemand\Conversations;
use App\Exports\SupplyDemandExport;
use App\Helpers\ResponseCode;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Facades\Excel;
class SupplyDemandController extends BaseController
{
public function __construct() { parent::__construct(new SupplyDemand()); }
private function error($text) { return $this->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');
}
}