|
|
|
|
|
<?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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|