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

1 year ago
<?php
namespace App\Http\Controllers\Admin;
use App\Models\SupplyDemand;
3 days ago
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;
1 year ago
use Illuminate\Support\Facades\Validator;
3 days ago
use Illuminate\Support\Carbon;
1 year ago
use Maatwebsite\Excel\Facades\Excel;
class SupplyDemandController extends BaseController
{
3 days ago
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);
1 year ago
}
3 days ago
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);
1 year ago
}
}
3 days ago
$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()]);
1 year ago
}
3 days ago
$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);
1 year ago
}
3 days ago
return $this->success(Profiles::demand($d->fresh(), true));
1 year ago
}
3 days ago
public function destroy() {
$d = SupplyDemand::find(request('id'));
if (!$d || !$d->is_proxy) return $this->error('仅可删除后台代发需求');
$d->delete(); return $this->success('删除成功,历史消息已保留');
1 year ago
}
3 days ago
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();
6 days ago
});
3 days ago
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('无法读取文件,请检查模板和日期格式'); }
1 year ago
}
3 days ago
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);
1 year ago
}
3 days ago
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]); });
1 year ago
}
3 days ago
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)];
1 year ago
}
3 days ago
return $this->success($out);
1 year ago
}
3 days ago
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);}); });
1 year ago
}
3 days ago
$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()?'已回复':'未回复'];
1 year ago
}
3 days ago
return Excel::download(new SupplyDemandExport($rows,['需求编号','需求标题','需求内容','发布时间','绑定状态','发布人姓名','发布人公司','发布人联系方式','发布人报名课程','发布人所在班级','对接人姓名','对接人公司','对接人联系方式','对接人报名课程','对接人所在班级','首次对接时间','最近对接时间','回复状态']),'供需交互统计.xlsx');
1 year ago
}
1 year ago
}