master
lion 2 days ago
parent c43eec2354
commit ba9266f8b0

@ -0,0 +1,23 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
class SupplyDemandImportTemplate extends SupplyDemandExport implements WithEvents
{
public function registerEvents(): array {
return [AfterSheet::class => function (AfterSheet $event) {
$sheet=$event->sheet->getDelegate();$sheet->freezePane('A2');
$sheet->getStyle('A1:J1')->getFont()->setBold(true);
foreach (['A'=>14,'B'=>28,'C'=>60,'D'=>32,'E'=>16,'F'=>30,'G'=>20,'H'=>22,'I'=>24,'J'=>32] as $col=>$width) $sheet->getColumnDimension($col)->setWidth($width);
$tips=['A'=>'必填:下拉选择供应、需求或投融资。','C'=>'必填:详细内容,不受200字限制,最多10000字。','D'=>'选填:多个标签使用“、”分隔,例如:新能源、新材料。','G'=>'必填:11位手机号,用于绑定学员;不公开。','H'=>'选填:到期日期格式为2027-01-31,到期当天仍有效;留空为长期有效。','I'=>'选填:微信号,不公开。','J'=>'选填:邮箱地址,不公开。'];
foreach ($tips as $col=>$tip) $sheet->getComment($col.'1')->getText()->createTextRun($tip);
for($row=2;$row<=1001;$row++) {
$v=$sheet->getCell('A'.$row)->getDataValidation();
$v->setType(DataValidation::TYPE_LIST)->setErrorStyle(DataValidation::STYLE_STOP)->setAllowBlank(false)->setShowDropDown(true)->setShowErrorMessage(true)->setErrorTitle('供需类型不正确')->setError('请选择供应、需求或投融资')->setFormula1('"供应,需求,投融资"');
foreach(['D','H'] as $col) $sheet->getCell($col.$row)->getDataValidation()->setShowInputMessage(true)->setPromptTitle($col==='D'?'行业标签':'有效期')->setPrompt($tips[$col]);
}
$sheet->getStyle('G2:J1001')->getNumberFormat()->setFormatCode('@');
}];
}
}

@ -28,6 +28,7 @@ class SupplyDemandController extends BaseController
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') === 'manual_review') $q->where('is_proxy', 1)->where('user_id', 0)->whereNotNull('binding_note')->where('binding_note', '!=', '');
if (request('binding') === 'bound') $q->where('user_id', '>', 0);
return $q;
}
@ -47,6 +48,7 @@ class SupplyDemandController extends BaseController
$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']);
if (isset($data['tag'])) $data['tag'] = implode(',', array_unique(array_filter(array_map('trim', preg_split('/[、,,]+/u', $data['tag'])), 'strlen')));
$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) {
@ -92,19 +94,46 @@ class SupplyDemandController extends BaseController
return $this->success(['count'=>$count]);
} catch (\InvalidArgumentException $e) { return $this->error($e->getMessage()); }
}
public function batchUnpublish() {
$ids=request('ids',[]);
if (Validator::make(['ids'=>$ids],['ids'=>'required|array|min:1|max:1000','ids.*'=>'required|integer|distinct'])->fails()) return $this->error('请选择需要取消发布的供需');
try {
$count=DB::transaction(function()use($ids){
$rows=SupplyDemand::whereIn('id',$ids)->orderBy('id')->lockForUpdate()->get();
if($rows->count()!==count($ids)) throw new \InvalidArgumentException('部分供需已不存在,请刷新');
$count=0;
foreach($rows as $d) if(in_array((int)$d->status,[0,1],true)){$d->status=4;$d->save();$count++;}
return $count;
});
return $this->success(['count'=>$count,'skipped_count'=>count($ids)-$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文件');
$stage = '读取Excel';
try {
if (!defined(ImportRows::class . '::KEYS')) return $this->error('导入程序版本不一致,请同步上传 app/Services/SupplyDemand/ImportRows.php 后重试');
$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));
if (array_slice($header ?: [],0,count(ImportRows::HEADERS)) !== ImportRows::HEADERS) return $this->error('模板表头不符,请下载模板后填写');
$keys=ImportRows::KEYS; $input=[];
foreach ($rows as $r) {
$values = array_slice(array_pad($r,count($keys),''),0,count($keys));
if (!count(array_filter($values, function ($x) { return $x !== null && trim((string) $x) !== ''; }))) continue;
$input[]=array_combine($keys,$values);
if (count($input)>1000) return $this->error('每次最多导入1000条');
}
if (!$input) return $this->error('模板中没有可导入的数据,请从第2行开始填写');
$stage = '校验数据与检查重复';
return $this->success(ImportRows::validate($input));
} catch (\Throwable $e) { return $this->error('无法读取文件,请检查模板和日期格式'); }
} catch (\Throwable $e) {
$errorId = bin2hex(random_bytes(4));
\Illuminate\Support\Facades\Log::error('供需导入预览失败', ['error_id'=>$errorId,'stage'=>$stage,'exception'=>get_class($e),'message'=>$e->getMessage()]);
if ($e instanceof \Illuminate\Database\QueryException) return $this->error('导入数据库检查失败,请确认供需升级迁移已执行(排查编号:'.$errorId.')');
return $this->error($stage.'失败,请联系管理员查看日志(排查编号:'.$errorId.')');
}
}
public function importRows() {
try {
@ -122,24 +151,29 @@ class SupplyDemandController extends BaseController
} 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 importTemplate() { return Excel::download(new \App\Exports\SupplyDemandImportTemplate([], 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)); }
if (!request()->boolean('identity_confirmed')) return $this->error('请核实学员姓名、公司和手机号后确认绑定');
try {
$result=Handover::bindRelated($d,$u,$this->getUserId());
return $this->success(Profiles::demand($result['demand'],true) + ['bound_count'=>$result['count']]);
}
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); }));
$users=User::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) + ['mobile_verified'=>(bool) $u->supply_mobile_verified_at]; }));
}
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 (isset($data['sms_content']) && is_string($data['sms_content'])) $data['sms_content'] = trim($data['sms_content']);
$v=Validator::make($data,['detail_tip'=>'required|string|max:500','chat_tip'=>'required|string|max:500','sent_tip'=>'required|string|max:200','sms_content'=>'sometimes|required|string|max:500','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]); });
}

@ -55,7 +55,7 @@ class SupplyDemandController extends CommonController
if (request()->has('status') && request('status') != -1) $q->where('status', request('status'));
} else { $q->where('status', 1); }
if (request('type')) $q->where('type', request('type'));
if (request('keyword')) $q->where(function ($s) { $s->where('title', 'like', '%' . request('keyword') . '%')->orWhere('content', 'like', '%' . request('keyword') . '%'); });
if (request('keyword')) $q->where(function ($s) { $s->where('title', 'like', '%' . request('keyword') . '%')->orWhere('content', 'like', '%' . request('keyword') . '%')->orWhere('tag', 'like', '%' . request('keyword') . '%'); });
if (request('expire_type')) {
if (request('expire_type') == 1) $q->where(function ($s) { $s->whereNull('expire_time')->orWhere('expire_time', '>', now()); });
else $q->where('expire_time', '<=', now());
@ -131,7 +131,7 @@ class SupplyDemandController extends CommonController
$all['user_id'] = $this->getUserId();
// 短信通知
$supply_demand_mobile = Config::getValueByKey('supply_demand_mobile');
$supply_demand_mobile = explode(',', $supply_demand_mobile);
$supply_demand_mobile = array_filter(array_map('trim', explode(',', (string) $supply_demand_mobile)), function ($mobile) { return preg_match('/^1\d{10}$/', $mobile); });
$smsSign = Config::getValueByKey('sms_sign');
$content = "{$smsSign}【{$this->getUser()->name}】发布了一条新的供需信息";
foreach ($supply_demand_mobile as $mobile) {

@ -193,6 +193,8 @@ class UserController extends CommonController
}
$model->fill($all);
$model->save();
// 手机验证发生在填写注册资料之前,资料保存后重新核对代发需求。
\App\Services\SupplyDemand\Handover::autoBind($model);
// 如果有公司信息,就更新一下公司
if (isset($all['company_name']) && !empty($all['company_name']) && $oldCompanyName != $all['company_name']) {
$model->company_id = null;

@ -37,6 +37,16 @@ class Handover
$user->supply_mobile_verified_at = now(); $user->save();
return self::autoBind($user);
}
private static function identityMismatch(SupplyDemand $d, User $user) {
$issues = [];
foreach (['姓名'=>[$d->publisher_name, $user->name], '公司'=>[$d->publisher_company, $user->company_name]] as $label=>$values) {
$imported = preg_replace('/^\s+|\s+$/u', '', (string) $values[0]);
$actual = preg_replace('/^\s+|\s+$/u', '', (string) $values[1]);
if ($imported === '' || $actual === '') $issues[] = $label.'信息不完整';
elseif ($imported !== $actual) $issues[] = $label.'不一致';
}
return $issues ? implode('、', $issues).',请人工核实' : null;
}
public static function autoBind(User $user) {
$mobile = $user->getRawOriginal('mobile');
if (!$user->supply_mobile_verified_at || !preg_match('/^1\d{10}$/', (string) $mobile)) return 0;
@ -46,16 +56,38 @@ class Handover
}
$count = 0;
foreach (SupplyDemand::where('is_proxy', 1)->where('user_id', 0)->where('publisher_mobile', $mobile)->get() as $d) {
$reason = self::identityMismatch($d, $user);
if ($reason) { $d->binding_note = $reason; $d->save(); continue; }
self::bind($d, $user); $count++;
}
return $count;
}
public static function bind(SupplyDemand $d, User $user) {
return DB::transaction(function () use ($d, $user) {
public static function bindRelated(SupplyDemand $selected, User $user, $adminId) {
return DB::transaction(function () use ($selected, $user, $adminId) {
$rows = SupplyDemand::where('is_proxy',1)->where('user_id',0)->where('publisher_mobile',$selected->publisher_mobile)->orderBy('id')->lockForUpdate()->get();
$name = trim((string) $selected->publisher_name);
$company = trim((string) $selected->publisher_company);
if (!$rows->contains('id',$selected->id)) throw new \InvalidArgumentException('该需求已绑定或已删除,请刷新');
$count = 0;
foreach ($rows as $row) {
if ($row->id !== $selected->id && (!$name || !$company || trim((string) $row->publisher_name) !== $name || trim((string) $row->publisher_company) !== $company)) continue;
self::bind($row,$user,$adminId);$count++;
}
return ['demand'=>$selected->fresh(),'count'=>$count];
});
}
public static function bind(SupplyDemand $d, User $user, $confirmedByAdminId = null) {
return DB::transaction(function () use ($d, $user, $confirmedByAdminId) {
$d = SupplyDemand::whereKey($d->id)->lockForUpdate()->firstOrFail();
if (!$d->is_proxy || $d->user_id) throw new \InvalidArgumentException('仅可绑定尚未认领的代发需求');
if (!$user->supply_mobile_verified_at) throw new \InvalidArgumentException('请先让该学员完成手机号验证');
$d->user_id = $user->id; $d->bound_at = now(); $d->binding_note = null; $d->save();
if (!preg_match('/^1\d{10}$/', (string) $user->getRawOriginal('mobile'))) throw new \InvalidArgumentException('学员手机号无效,请先完善学员资料');
if ($confirmedByAdminId === null && !$user->supply_mobile_verified_at) throw new \InvalidArgumentException('请先让该学员完成手机号验证');
if ($confirmedByAdminId === null) {
if ((string) $d->publisher_mobile !== (string) $user->getRawOriginal('mobile')) throw new \InvalidArgumentException('手机号不一致,请人工核实');
$reason = self::identityMismatch($d, $user);
if ($reason) throw new \InvalidArgumentException($reason);
}
$d->user_id = $user->id; $d->bound_at = now(); $d->binding_note = $confirmedByAdminId !== null ? '管理员人工核实绑定:'.$confirmedByAdminId : null; $d->save();
foreach (Dialogue::where('supply_demand_id', $d->id)->where('to_user_id', 0)->lockForUpdate()->get() as $dialogue) {
// 本人此前咨询的需求在认领后保留历史,禁止继续向自己发消息。
$dialogue->to_user_id = $user->id;

@ -4,16 +4,19 @@ use App\Models\SupplyDemand;
use Illuminate\Support\Facades\Validator;
class ImportRows
{
public const HEADERS = ['标题','需求内容','行业标签','发布人姓名','公司','手机号','有效期'];
public const HEADERS = ['供需类型','标题','详细内容','行业标签','发布人姓名','公司','手机号','有效期','微信','邮箱'];
public const KEYS = ['type','title','content','tag','publisher_name','publisher_company','publisher_mobile','expire_time','wechat','email'];
public static function validate(array $rows) {
if (!$rows || count($rows) > 1000) throw new \InvalidArgumentException('每次导入1至1000条需求');
$valid = []; $errors = []; $seen = [];
foreach (array_values($rows) as $i => $r) {
if (!is_array($r)) { $errors[] = ['row' => $i + 2, 'message' => '行数据格式错误']; continue; }
$data = array_intersect_key($r, array_flip(['title','content','tag','publisher_name','publisher_company','publisher_mobile','expire_time']));
$data = array_intersect_key($r, array_flip(self::KEYS));
foreach ($data as $key => $v) $data[$key] = is_scalar($v) ? trim((string) $v) : '';
$data['type'] = ['供应'=>1,'需求'=>2,'投融资'=>3][$data['type'] ?? '需求'] ?? ($data['type'] ?? 2);
$data['tag'] = implode(',', array_unique(array_filter(array_map('trim', preg_split('/[、,,]+/u', $data['tag'] ?? '')), 'strlen')));
if (empty($data['expire_time'])) $data['expire_time'] = null;
$v = Validator::make($data, ['title'=>'required|string|max:100','content'=>'required|string|max:10000',
$v = Validator::make($data, ['type'=>'required|integer|in:1,2,3','wechat'=>'nullable|string|max:100','email'=>'nullable|email|max:255','title'=>'required|string|max:100','content'=>'required|string|max:10000',
'tag'=>'nullable|string|max:255','publisher_name'=>'required|string|max:100', 'publisher_company'=>'required|string|max:255',
'publisher_mobile'=>['required','regex:/^1\d{10}$/'],'expire_time'=>'nullable|date_format:Y-m-d']);
if ($v->fails()) { $errors[] = ['row' => $i + 2, 'message' => implode(';', $v->errors()->all())]; continue; }

@ -26,7 +26,7 @@ class Notices
if (!$user || !$hasUnread) {
DB::table('supply_demand_sms_notices')->where('id', $row->id)->update(['pending_at' => null]); return false;
}
$content = (Config::getValueByKey('sms_sign') ?: '') . '您在STBC小程序发布的对接需求有新的对接,请登录小程序,在“校友权益—供需对接—我的私信”中查看并回复。';
$content = (Config::getValueByKey('sms_sign') ?: '') . $settings['sms_content'];
try { $ok = ($sender ?: function ($mobile, $text) { return ymSms($mobile, $text, 0, 'supply_demand', null); })($user->getRawOriginal('mobile'), $content); }
catch (\Throwable $e) { $ok = false; }
DB::table('supply_demand_sms_notices')->where('id', $row->id)->update([

@ -13,7 +13,7 @@ class Profiles
public static function user(?User $user, $private = false) {
if (!$user) return null;
$user->loadMissing(['courseSigns.course.typeDetail']);
$sign = $user->courseSigns->filter(function ($s) { return (int) $s->status === 1 && $s->course; })
$sign = $user->courseSigns->filter(function ($s) use ($user) { return (string) $s->user_id === (string) $user->id && (int) $s->status === 1 && !$s->trashed() && $s->course; })
->sort(function ($a, $b) {
$rank = self::rank(optional($a->course->typeDetail)->name) <=> self::rank(optional($b->course->typeDetail)->name);
return $rank ?: (strcmp((string) $b->created_at, (string) $a->created_at) ?: ($b->id <=> $a->id));
@ -33,12 +33,6 @@ class Profiles
}
$profile = ['id' => 0, 'name' => $d->publisher_name ?: '平台代发', 'company_name' => $d->publisher_company,
'headimgurl' => '', 'course_name' => '', 'class_name' => ''];
$candidates = User::where('mobile', $d->publisher_mobile)->limit(2)->get();
if ($candidates->count() === 1) {
$academic = self::user($candidates->first());
$profile['course_name'] = $academic['course_name'];
$profile['class_name'] = $academic['class_name'];
}
if ($private) $profile['mobile'] = $d->publisher_mobile;
return $profile;
}

@ -7,6 +7,7 @@ class Settings
'detail_tip' => '该需求目前由系统代管,发布人将在开班并完成账号绑定后陆续回复。',
'chat_tip' => '目前为系统接管中,您的对接消息将被保存,待发布人完成账号绑定后处理。',
'sent_tip' => '对接信息已提交,请耐心等待回复。',
'sms_content' => '您在STBC小程序发布的对接需求有新的对接,请登录小程序,在“校友权益—供需对接—我的私信”中查看并回复。',
'sms_enabled' => '1', 'sms_interval_minutes' => '30', 'sms_daily_limit' => '3',
];
public static function all() {

@ -186,18 +186,30 @@ class SurveySchema
return $map;
}
// Input must be in the same sort/id order used for questionnaire rendering.
public static function conditionValues(array $condition): array
{
return array_map('strval', isset($condition['values']) ? $condition['values'] : [$condition['value'] ?? '']);
}
// Conditions may only refer to earlier choice questions, preventing circular dependencies.
public static function validateConditions(array $questions): void
{
$preceding = [];
foreach ($questions as $q) {
$condition = $q['config']['visible_when'] ?? null;
if ($condition !== null) {
if (!is_array($condition) || !is_string($condition['field'] ?? null) || !is_scalar($condition['value'] ?? null)) throw new InvalidArgumentException('显示条件格式不正确');
if (!is_array($condition) || !is_string($condition['field'] ?? null)) throw new InvalidArgumentException('显示条件格式不正确');
if (array_key_exists('values', $condition)) {
if (!is_array($condition['values']) || !$condition['values'] || count(array_filter($condition['values'], fn ($v) => !is_scalar($v)))) throw new InvalidArgumentException('请选择至少一个触发显示的选项');
} elseif (!is_scalar($condition['value'] ?? null)) throw new InvalidArgumentException('显示条件格式不正确');
$parent = $preceding[$condition['field']] ?? null;
if ($q['edit_input'] === 'section' || !$parent || !in_array($parent['edit_input'], ['radio', 'checkbox'], true)) throw new InvalidArgumentException($q['name'].':显示条件只能引用本题之前的单选或多选题,请先调整条件');
$keys = array_map(fn ($o) => (string) $o['key'], self::arrayValue($parent['select_item'] ?? []));
if (!in_array((string) $condition['value'], $keys, true)) throw new InvalidArgumentException($q['name'].':显示条件引用的选项不存在,请先调整条件');
$values = self::conditionValues($condition);
if (array_diff($values, $keys)) throw new InvalidArgumentException($q['name'].':显示条件引用的选项不存在,请先调整条件');
if (array_key_exists('required_values', $condition)) {
if (!is_array($condition['required_values']) || count(array_filter($condition['required_values'], fn ($v) => !is_scalar($v))) || array_diff(array_map('strval', $condition['required_values']), $values)) throw new InvalidArgumentException('必填触发选项必须属于显示触发选项');
}
}
$preceding[$q['field']] = $q;
}
@ -213,13 +225,33 @@ class SurveySchema
foreach ($questions as $parent) {
if (($parent['field'] ?? '') !== ($condition['field'] ?? '')) continue;
if (!self::visible($parent, $questions, $map, $keyed, $visited)) return false;
$value = $map[(string) $parent['id']]['value'] ?? null;
$keys = self::selectedKeys($parent, $value, $keyed);
return in_array((string) ($condition['value'] ?? ''), $keys, true);
$keys = self::selectedKeys($parent, $map[(string) $parent['id']]['value'] ?? null, $keyed);
return (bool) array_intersect(self::conditionValues($condition), $keys);
}
return false;
}
public static function requiredFor(array $q, array $questions, array $map, bool $keyed = true): bool
{
$condition = $q['config']['visible_when'] ?? null;
if (!$condition || !array_key_exists('required_values', $condition)) return self::required($q);
if (!self::visible($q, $questions, $map, $keyed)) return false;
foreach ($questions as $parent) {
if (($parent['field'] ?? '') !== $condition['field']) continue;
return (bool) array_intersect(array_map('strval', $condition['required_values']), self::selectedKeys($parent, $map[(string) $parent['id']]['value'] ?? null, $keyed));
}
return false;
}
private static function applyRequired(array $q, array $questions, array $map, bool $keyed): array
{
$q['need_fill'] = self::requiredFor($q, $questions, $map, $keyed);
$rules = array_values(array_filter(explode(',', $q['rule'] ?? ''), fn ($rule) => $rule !== 'required' && $rule !== ''));
if ($q['need_fill']) $rules[] = 'required';
$q['rule'] = implode(',', $rules);
return $q;
}
public static function normalize(array $questions, $data, bool $keyed = false): array
{
$map = self::answerMap($data);
@ -235,6 +267,7 @@ class SurveySchema
continue;
}
$q['hidden'] = false;
$q = self::applyRequired($q, $questions, $map, $keyed);
if ($q['edit_input'] === 'section') {
$q['value'] = null;
} elseif ($q['edit_input'] === 'multi_dimension') {
@ -417,6 +450,7 @@ class SurveySchema
$q['hidden'] = !self::visible($q, $questions, $map, $keyed);
$q['option_texts'] = self::arrayValue($raw['option_texts'] ?? []);
$q['other_text'] = $raw['other_text'] ?? $raw['allow_input_text'] ?? '';
$q = self::applyRequired($q, $questions, $map, $keyed);
$q['display_value'] = self::display($q, $q['value'], $keyed);
$q['selected_keys'] = self::selectedKeys($q, $q['value'], $keyed);
if ($q['edit_input'] === 'multi_dimension') {

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -239,6 +239,7 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
// 供需信息
Route::post('supply-demand/batch-publish', [\App\Http\Controllers\Admin\SupplyDemandController::class, "batchPublish"]);
Route::post('supply-demand/batch-unpublish', [\App\Http\Controllers\Admin\SupplyDemandController::class, "batchUnpublish"]);
Route::post('supply-demand/import-preview', [\App\Http\Controllers\Admin\SupplyDemandController::class, "importPreview"]);
Route::post('supply-demand/import-rows', [\App\Http\Controllers\Admin\SupplyDemandController::class, "importRows"]);
Route::get('supply-demand/import-template', [\App\Http\Controllers\Admin\SupplyDemandController::class, "importTemplate"]);
@ -410,6 +411,10 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () {
Route::get('course/course-content-detail', [\App\Http\Controllers\Mobile\CourseController::class, "courseContentDetail"]);
// 支付回调
Route::any('course/pay_callback', [\App\Http\Controllers\Mobile\CourseController::class, "payCallback"]);
// 图书目录与详情允许游客查询。
Route::get('book/other', [\App\Http\Controllers\Mobile\BookController::class, "other"]);
Route::get('book/index', [\App\Http\Controllers\Mobile\BookController::class, "index"]);
Route::get('book/detail', [\App\Http\Controllers\Mobile\BookController::class, "detail"]);
// 分享页面允许游客浏览,用户操作仍需完成注册。
Route::get('supply-demand/index', [\App\Http\Controllers\Mobile\SupplyDemandController::class, "index"])->middleware(\App\Http\Middleware\SupplyDemandAccess::class . ":optional");
Route::get('supply-demand/detail', [\App\Http\Controllers\Mobile\SupplyDemandController::class, "detail"])->middleware(\App\Http\Middleware\SupplyDemandAccess::class . ":optional");
@ -479,9 +484,6 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () {
Route::get('supply-demand/un-keep-supply-demand', [\App\Http\Controllers\Mobile\SupplyDemandController::class, "unKeepSupplyDemand"])->middleware(\App\Http\Middleware\SupplyDemandAccess::class);
// 图书前台接口
Route::get('book/other', [\App\Http\Controllers\Mobile\BookController::class, "other"]);
Route::get('book/index', [\App\Http\Controllers\Mobile\BookController::class, "index"]);
Route::get('book/detail', [\App\Http\Controllers\Mobile\BookController::class, "detail"]);
});

@ -36,7 +36,7 @@ class SupplyDemandHandoverTest extends TestCase
Carbon::setTestNow(Carbon::parse('2026-10-16 15:00:00'));
}
protected function tearDown(): void { Carbon::setTestNow();Auth::guard('mobile')->forgetUser();Auth::guard('admin')->forgetUser();DB::disconnect('sqlite');parent::tearDown(); }
private function demand($owner=0,$status=1) { return SupplyDemand::create(['user_id'=>$owner,'status'=>$status,'is_proxy'=>1,'publisher_name'=>'发布人','publisher_company'=>'导入公司','publisher_mobile'=>'13800000001','title'=>'外部需求','content'=>'采购设备','public_way'=>3]); }
private function demand($owner=0,$status=1) { return SupplyDemand::create(['user_id'=>$owner,'status'=>$status,'is_proxy'=>1,'publisher_name'=>'学员1','publisher_company'=>'公司1','publisher_mobile'=>'13800000001','title'=>'外部需求','content'=>'采购设备','public_way'=>3]); }
private function actAs($id) { Auth::guard('mobile')->setUser(User::find($id)); }
public function test_managed_messages_transfer_without_copying_and_owner_can_reply() {
$d=$this->demand();$sent=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'我可以供应']);
@ -48,6 +48,32 @@ class SupplyDemandHandoverTest extends TestCase
$this->assertSame(2,Message::count());$this->assertSame(2,Message::latest('id')->first()->to_user_id);
$this->assertSame(1,DB::table('supply_demand_sms_notices')->count());
}
public function test_auto_binding_requires_matching_name_company_and_preserves_pending_messages() {
$matching=$this->demand();$name=$this->demand();$company=$this->demand();$incomplete=$this->demand();
$name->publisher_name='其他人';$name->save();
$company->publisher_company='其他公司';$company->save();
$incomplete->publisher_company='';$incomplete->save();
Conversations::send(2,['supply_demand_id'=>$name->id,'content'=>'先保存咨询']);
$this->assertSame(1,Handover::verified(User::find(1)));
$this->assertSame(1,(int)$matching->fresh()->user_id);
foreach ([$name,$company,$incomplete] as $d) $this->assertSame(0,(int)$d->fresh()->user_id);
$this->assertStringContainsString('姓名不一致',$name->fresh()->binding_note);
$this->assertStringContainsString('公司不一致',$company->fresh()->binding_note);
$this->assertStringContainsString('公司信息不完整',$incomplete->fresh()->binding_note);
$this->assertSame(0,Message::first()->to_user_id);
$this->assertSame(0,DB::table('supply_demand_sms_notices')->count());
$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$name->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('user_id',1);
$this->assertSame(1,Message::first()->to_user_id);$this->assertSame(1,Message::count());
}
public function test_profile_completion_retries_verified_account_binding_and_ignores_edge_whitespace() {
$d=$this->demand();$user=User::find(1);$user->name=null;$user->company_name=null;$user->save();
$this->assertSame(0,Handover::verified($user));
$this->assertStringContainsString('信息不完整',$d->fresh()->binding_note);
$user->name=' 学员1 ';$user->company_name=' 公司1 ';$user->save();
$this->assertSame(1,Handover::autoBind($user));
$this->assertNull($d->fresh()->binding_note);
}
public function test_unverified_and_duplicate_phones_do_not_auto_bind() {
$d=$this->demand();$this->assertSame(0,Handover::autoBind(User::find(1)));
User::create(['name'=>'重复手机号','mobile'=>'13800000001']);Handover::verified(User::find(1));
@ -75,6 +101,24 @@ class SupplyDemandHandoverTest extends TestCase
$a=$this->demand();$b=$this->demand();Conversations::send(2,['supply_demand_id'=>$a->id,'content'=>'一']);
Config::create(['key'=>'message_limit','value'=>'1']);$this->expectException(\InvalidArgumentException::class);Conversations::send(2,['supply_demand_id'=>$b->id,'content'=>'二']);
}
public function test_customer_uploaded_template_previews_three_valid_rows() {
$path=base_path('../../需求文档/需求导入模板 (1).xlsx');
if (!is_file($path)) { $this->markTestSkipped('客户附件仅用于本地回归'); }
$file=new \Illuminate\Http\UploadedFile($path,'需求导入模板.xlsx','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',null,true);
$this->post('/api/admin/supply-demand/import-preview',['file'=>$file])->assertOk()->assertJsonPath('total',3)->assertJsonCount(3,'rows')->assertJsonCount(0,'errors');
}
public function test_import_types_contacts_tags_and_long_content() {
$row=['type'=>'供应','title'=>'设备供应','content'=>str_repeat('详细内容',100),'tag'=>'新能源、新材料,制造,新能源','wechat'=>'wx-test','email'=>'test@example.com','publisher_name'=>'张同学','publisher_company'=>'公司','publisher_mobile'=>'13900000001','expire_time'=>'2027-01-31'];
$this->postJson('/api/admin/supply-demand/import-rows',['rows'=>[$row]])->assertJsonPath('imported',1);
$d=SupplyDemand::first();$this->assertSame(1,(int)$d->type);$this->assertSame('新能源,新材料,制造',$d->tag);$this->assertSame($row['content'],$d->content);$this->assertSame('wx-test',$d->wechat);$this->assertSame('test@example.com',$d->email);
$row['type']='未知';$this->assertCount(1,ImportRows::validate([$row])['errors']);
$export=new \App\Exports\SupplyDemandImportTemplate([],ImportRows::HEADERS);
$bytes=\Maatwebsite\Excel\Facades\Excel::raw($export,\Maatwebsite\Excel\Excel::XLSX);
$file=tempnam(sys_get_temp_dir(),'supply-template');file_put_contents($file,$bytes);
try { $sheet=\PhpOffice\PhpSpreadsheet\IOFactory::load($file)->getActiveSheet();$this->assertSame('"供应,需求,投融资"',$sheet->getCell('A2')->getDataValidation()->getFormula1());$this->assertSame('详细内容',$sheet->getCell('C1')->getValue());$this->assertStringContainsString('、',$sheet->getCell('D2')->getDataValidation()->getPrompt()); } finally { unlink($file); }
}
public function test_import_validation_duplicate_and_pending_publish() {
$row=['title'=>'设备采购','content'=>'一批设备','publisher_name'=>'张同学','publisher_company'=>'公司','publisher_mobile'=>'13900000001'];
$v=ImportRows::validate([$row,$row]);$this->assertCount(1,$v['errors']);
@ -97,6 +141,35 @@ class SupplyDemandHandoverTest extends TestCase
$res->assertJsonPath('supplyDemand',null)->assertJsonPath('message.data.0.supply_demand',null);
$this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertOk()->assertJsonPath('errcode',10002);
}
public function test_no_enrollment_never_inherits_another_users_class() {
DB::table('course_types')->insert(['id'=>1,'name'=>'高研班']);
DB::table('courses')->insert(['id'=>1,'type'=>1,'name'=>'第十期高研班','class'=>'第十期']);
DB::table('course_signs')->insert(['id'=>1,'user_id'=>1,'course_id'=>1,'status'=>1,'created_at'=>now()]);
$user=User::find(2);$profile=Profiles::user($user);
$this->assertSame('',$profile['class_name']);$this->assertSame('',$profile['course_name']);
$user->setRelation('courseSigns',User::find(1)->courseSigns);
$this->assertSame('',Profiles::user($user)['class_name']);
$d=$this->demand(2);$this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.data.0.publisher.class_name','');
}
public function test_unbound_publisher_hides_matched_courses_and_keeps_imported_company_until_binding() {
DB::table('course_types')->insert(['id'=>1,'name'=>'高研班']);
DB::table('courses')->insert(['id'=>1,'type'=>1,'name'=>'高研课程','class'=>'高研一班']);
DB::table('course_signs')->insert(['id'=>1,'user_id'=>1,'course_id'=>1,'status'=>1,'created_at'=>now()]);
$user=User::find(1);$user->company_name='账号公司';$user->save();
$d=$this->demand();$d->publisher_mobile=$user->getRawOriginal('mobile');$d->publisher_company='导入公司';$d->save();
foreach ([false,true] as $private) {
$profile=Profiles::publisher($d,$private);
$this->assertSame('',$profile['course_name']);$this->assertSame('',$profile['class_name']);$this->assertSame('导入公司',$profile['company_name']);
}
$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('publisher.class_name','高研一班')->assertJsonPath('publisher.company_name','账号公司');
$this->getJson('/api/mobile/supply-demand/detail?id='.$d->id)->assertJsonPath('publisher.class_name','高研一班')->assertJsonPath('publisher.company_name','账号公司');
$this->assertSame('导入公司',$d->fresh()->publisher_company);
$this->assertSame('账号公司',$user->fresh()->company_name);
$user->company_name=null;$user->save();
$this->assertSame('导入公司',Profiles::publisher($d->fresh())['company_name']);
}
public function test_class_priority_approved_only_and_latest_within_system() {
foreach([[1,'初创班'],[2,'高研班'],[3,'其他课程']] as [$id,$name]) DB::table('course_types')->insert(['id'=>$id,'name'=>$name]);
foreach([[1,1,'初创一班'],[2,2,'高研旧班'],[3,2,'高研新班'],[4,2,'待审核班'],[5,3,'其他新班']] as [$id,$type,$name]) DB::table('courses')->insert(['id'=>$id,'type'=>$type,'name'=>$name,'class'=>$name]);
@ -106,6 +179,22 @@ class SupplyDemandHandoverTest extends TestCase
$this->assertSame('其他新班',Profiles::user(User::find(1))['class_name']);
DB::table('course_signs')->update(['status'=>0]);$this->assertSame('',Profiles::user(User::find(1))['class_name']);
}
public function test_sms_body_can_be_configured_without_sending_on_save() {
$settings=\App\Services\SupplyDemand\Settings::all();
$this->assertStringContainsString('我的私信',$settings['sms_content']);
$settings['sms_content']='您有新的供需对接,请登录查看。';
$this->postJson('/api/admin/supply-demand/settings',$settings)->assertJsonPath('sms_content',$settings['sms_content']);
$this->assertSame(0,DB::table('supply_demand_sms_notices')->count());
Config::create(['key'=>'sms_sign','value'=>'【测试签名】']);
$d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']);
$calls=0;
$this->assertTrue(Notices::deliver(1,function($mobile,$text)use(&$calls){$calls++;$this->assertSame('【测试签名】您有新的供需对接,请登录查看。',$text);return true;}));
$this->assertSame(1,$calls);
$settings['sms_content']=' ';
$this->postJson('/api/admin/supply-demand/settings',$settings)->assertJsonPath('errcode',10002);
$this->assertSame('您有新的供需对接,请登录查看。',\App\Services\SupplyDemand\Settings::all()['sms_content']);
}
public function test_sms_uses_existing_channel_contract_cooldown_and_failure_is_retryable() {
$d=$this->demand(1);Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'咨询']);
$calls=0;$sender=function($mobile,$text) use(&$calls) {$calls++;$this->assertSame('13800000001',$mobile);$this->assertStringContainsString('校友权益—供需对接—我的私信',$text);return true;};
@ -117,10 +206,54 @@ class SupplyDemandHandoverTest extends TestCase
$a=Conversations::send(2,['supply_demand_id'=>$d->id,'content'=>'今天咨询']);Conversations::send(1,['dialogue_id'=>$a['dialogue_id'],'content'=>'今天回复']);
$this->getJson('/api/admin/supply-demand/chart?start_date=2026-10-16&end_date=2026-10-16')->assertOk()->assertJsonPath('message_count',2)->assertJsonPath('interaction_count',1)->assertJsonPath('list.total',1)->assertJsonPath('supply_demand_count',0);
}
public function test_manual_binding_links_same_imported_identity_demands_and_moves_all_their_messages() {
$a=$this->demand();$b=$this->demand();$otherName=$this->demand();$otherCompany=$this->demand();
$otherName->publisher_name='其他人';$otherName->save();$otherCompany->publisher_company='其他公司';$otherCompany->save();
Conversations::send(2,['supply_demand_id'=>$a->id,'content'=>'咨询A']);
Conversations::send(3,['supply_demand_id'=>$b->id,'content'=>'咨询B']);
Conversations::send(2,['supply_demand_id'=>$otherName->id,'content'=>'其他人咨询']);
$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$a->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('bound_count',2);
$this->assertSame(1,(int)$a->fresh()->user_id);$this->assertSame(1,(int)$b->fresh()->user_id);
$this->assertSame(0,(int)$otherName->fresh()->user_id);$this->assertSame(0,(int)$otherCompany->fresh()->user_id);
$this->assertSame(2,Message::where('to_user_id',1)->count());$this->assertSame(1,Message::where('to_user_id',0)->count());
$this->assertSame(3,Message::count());$this->assertSame(2,Dialogue::where('to_user_id',1)->count());
}
public function test_batch_unpublish_keeps_bindings_and_messages_and_excludes_public_listing() {
$published=$this->demand(1);$pending=$this->demand(0,0);$rejected=$this->demand(0,2);
Conversations::send(2,['supply_demand_id'=>$published->id,'content'=>'历史咨询']);
$this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[$published->id,$pending->id,$rejected->id]])->assertJsonPath('count',2)->assertJsonPath('skipped_count',1);
$this->assertSame(4,(int)$published->fresh()->status);$this->assertSame(4,(int)$pending->fresh()->status);$this->assertSame(2,(int)$rejected->fresh()->status);
$this->assertSame(1,(int)$published->fresh()->user_id);$this->assertSame(1,Message::count());
$this->getJson('/api/mobile/supply-demand/index')->assertJsonPath('supplyDemands.total',0);
$other=$this->demand();
$this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[$other->id,99999]])->assertJsonPath('errcode',10002);
$this->assertSame(1,(int)$other->fresh()->status);
$this->postJson('/api/admin/supply-demand/batch-unpublish',['ids'=>[]])->assertJsonPath('errcode',10002);
}
public function test_manual_review_filter_only_includes_unbound_proxy_rows_needing_review() {
$review=$this->demand();$review->binding_note='姓名不一致,请人工核实';$review->save();
$plain=$this->demand();$bound=$this->demand(1);$bound->binding_note='管理员人工核实绑定:1';$bound->save();
$this->getJson('/api/admin/supply-demand/index?binding=manual_review')->assertJsonPath('total',1)->assertJsonPath('data.0.id',$review->id);
$this->getJson('/api/admin/supply-demand/index?binding=unbound')->assertJsonPath('total',2);
$this->getJson('/api/admin/supply-demand/index?binding=bound')->assertJsonPath('total',1);
$this->getJson('/api/admin/supply-demand/index?binding=manual_review&filter[0][key]=title&filter[0][op]=like&filter[0][value]=不存在')->assertJsonPath('total',0);
}
public function test_manual_search_and_confirmed_binding_include_legacy_unverified_students() {
$d=$this->demand();$user=User::find(1);
$this->getJson('/api/admin/supply-demand/binding-users?keyword='.$user->getRawOriginal('mobile'))->assertJsonPath('0.id',1)->assertJsonPath('0.mobile_verified',false);
$this->assertSame(0,Handover::autoBind($user));
$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1])->assertJsonPath('errcode',10002);
$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1,'identity_confirmed'=>true])->assertJsonPath('user_id',1);
$this->assertNull($user->fresh()->supply_mobile_verified_at);
$this->assertStringContainsString('管理员人工核实绑定',$d->fresh()->binding_note);
}
public function test_settings_and_manual_binding_validation() {
$this->postJson('/api/admin/supply-demand/settings',['detail_tip'=>'详情提示','chat_tip'=>'顶部提示','sent_tip'=>'成功提示','sms_enabled'=>'0','sms_interval_minutes'=>15,'sms_daily_limit'=>2])->assertOk()->assertJsonPath('chat_tip','顶部提示');
$d=$this->demand();$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>1])->assertJsonPath('errcode',10002);
Handover::verified(User::find(3));$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>3])->assertOk()->assertJsonPath('user_id',3);
Handover::verified(User::find(3));$this->postJson('/api/admin/supply-demand/bind-user',['id'=>$d->id,'user_id'=>3,'identity_confirmed'=>true])->assertOk()->assertJsonPath('user_id',3);
}
public function test_provider_calls_stop_at_daily_cap_and_retry_next_day() {
@ -171,7 +304,7 @@ class SupplyDemandHandoverTest extends TestCase
public function test_real_excel_preview_preserves_mobile_and_imports_valid_date() {
$sheet=new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet->getActiveSheet()->fromArray([ImportRows::HEADERS,['采购设备','一批设备','制造','张同学','某公司','13900000001','2027-01-01']],null,'A1');
$sheet->getActiveSheet()->fromArray([ImportRows::HEADERS,['需求','采购设备','一批设备','制造','张同学','某公司','13900000001','2027-01-01','','']],null,'A1');
$path=tempnam(sys_get_temp_dir(),'demand');
try {
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($sheet))->save($path);
@ -204,6 +337,29 @@ class SupplyDemandHandoverTest extends TestCase
Schema::create('personal_access_tokens',function(Blueprint $t) { $t->id();$t->morphs('tokenable');$t->string('name');$t->string('token',64)->unique();$t->text('abilities')->nullable();$t->timestamp('last_used_at')->nullable();$t->timestamp('expires_at')->nullable();$t->timestamps(); });
$this->withMiddleware();config(['app.debug'=>false]);Auth::guard('mobile')->forgetUser();
}
public function test_keyword_search_matches_tags_without_bypassing_public_or_owner_filters() {
$published=$this->demand(1);$published->tag='新能源,人工智能';$published->save();
$draft=$this->demand(2,0);$draft->tag='人工智能';$draft->save();
$this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$published->id);
$this->getJson('/api/mobile/supply-demand/index?myself=1&keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$draft->id);
$this->getJson('/api/mobile/supply-demand/index?type=1&keyword='.urlencode('智能'))->assertJsonPath('supplyDemands.total',0);
$this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('外部需求'))->assertJsonPath('supplyDemands.total',1);
$this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('采购设备'))->assertJsonPath('supplyDemands.total',1);
$this->enableGuestRequests();
$this->getJson('/api/mobile/supply-demand/index?keyword='.urlencode('新能源'))->assertJsonPath('supplyDemands.total',1)->assertJsonPath('supplyDemands.data.0.id',$published->id);
}
public function test_library_read_endpoints_allow_guests_but_booking_submission_remains_protected() {
Schema::create('uploads',function(Blueprint $t) { $t->id();$t->softDeletes(); });
Schema::create('books',function(Blueprint $t) { $t->id();$t->string('title');$t->string('author')->nullable();$t->string('isbn')->nullable();$t->string('category');$t->integer('cover_id')->nullable();$t->timestamps();$t->softDeletes(); });
DB::table('books')->insert(['id'=>1,'title'=>'游客查询图书','category'=>'科技']);
$this->enableGuestRequests();
$this->getJson('/api/mobile/book/other')->assertOk()->assertJsonPath('category.0','科技');
$this->getJson('/api/mobile/book/index')->assertOk()->assertJsonPath('data.0.title','游客查询图书');
$this->getJson('/api/mobile/book/detail?id=1')->assertOk()->assertJsonPath('title','游客查询图书');
$this->postJson('/api/mobile/schedule/save',[])->assertJsonPath('errcode',40001);
}
public function test_shared_supply_pages_allow_guests_and_expired_tokens_but_not_drafts() {
$published=$this->demand();$draft=$this->demand(0,0);$this->enableGuestRequests();
$this->getJson('/api/mobile/supply-demand/index')->assertOk()->assertJsonPath('supplyDemands.total',1);
@ -222,6 +378,14 @@ class SupplyDemandHandoverTest extends TestCase
$this->withToken($token)->postJson('/api/mobile/supply-demand/save',['title'=>'未注册发布'])->assertJsonPath('errcode',10003);
$this->assertSame(0,Message::count());
}
public function test_registered_user_without_any_course_can_be_searched_and_publish() {
$this->assertSame(0,DB::table('course_signs')->where('user_id',2)->count());
$this->getJson('/api/admin/supply-demand/binding-users?keyword=13800000002')->assertJsonPath('0.id',2)->assertJsonPath('0.course_name','');
$this->enableGuestRequests();$token=User::find(2)->createToken('mobile-token')->plainTextToken;
$this->withToken($token)->postJson('/api/mobile/supply-demand/save',['title'=>'未报名用户发布','type'=>2,'content'=>'需求详情','status'=>0])->assertOk()->assertJsonPath('user_id',2);
$this->assertSame(1,SupplyDemand::where('user_id',2)->where('title','未报名用户发布')->count());
}
public function test_registered_member_keeps_private_access_after_optional_browse() {
$d=$this->demand(1,0);$this->enableGuestRequests();$token=User::find(1)->createToken('mobile-token')->plainTextToken;
$this->withToken($token)->getJson('/api/mobile/supply-demand/session')->assertJsonPath('registered',true)->assertJsonPath('user.id',1);

@ -65,6 +65,24 @@ class SurveyWorkflowTest extends TestCase
return $response->json('id');
}
public function testConditionalRequiredAnswersAreEnforcedOnSubmissionAndResubmission(): void
{
$base = '/api/admin/course-content-evaluation-ask/save';
$parentId = $this->postJson($base, ['course_content_evaluation_id' => 10, 'name' => '融资', 'field' => 'finance', 'edit_input' => 'radio', 'sort' => 1, 'select_item' => [['key' => 'later', 'value' => '事后对接'], ['key' => 'road', 'value' => '参加路演'], ['key' => 'no', 'value' => '不需要']]])->assertOk()->json('id');
$childId = $this->postJson($base, ['course_content_evaluation_id' => 10, 'name' => '材料', 'field' => 'material', 'edit_input' => 'text', 'sort' => 2, 'need_fill' => true, 'config' => ['visible_when' => ['field' => 'finance', 'values' => ['later', 'road'], 'required_values' => ['road']]]])->assertOk()->json('id');
$payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $parentId, 'value' => 'later'], ['id' => $childId, 'value' => '']]];
$url = '/api/mobile/course/course-content-form';
$replyId = $this->postJson($url, $payload)->assertJsonPath('data.1.need_fill', false)->json('id');
$payload['data'][0]['value'] = 'road';
$this->postJson($url, $payload)->assertJsonPath('errmsg', '材料:请填写此项');
$payload['data'][1]['value'] = '路演材料';
$this->postJson($url, $payload)->assertJsonPath('id', $replyId)->assertJsonPath('data.1.need_fill', true);
$this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertJsonPath('my_response.questions.1.need_fill', true);
$payload['data'][0]['value'] = 'no';
$this->postJson($url, $payload)->assertJsonPath('data.1.hidden', true)->assertJsonPath('data.1.value', null);
$this->assertSame(1, \App\Models\CourseContentEvaluationForm::count());
}
public function testSoftDeletingRepliesUpdatesCountsAndUnlocksOnlyAfterLastReply(): void
{
$id = $this->createQuestion();

@ -49,6 +49,34 @@ class SurveySchemaTest extends TestCase
SurveySchema::question($q);
}
public function testMultipleConditionTriggersAndRequiredOverrides(): void
{
$parent = ['id' => 1, 'name' => '是否需要融资', 'field' => 'finance', 'edit_input' => 'radio', 'select_item' => [['key' => 'later', 'value' => '事后对接'], ['key' => 'road', 'value' => '参加路演'], ['key' => 'no', 'value' => '不需要']]];
$child = ['id' => 2, 'name' => '材料', 'field' => 'material', 'edit_input' => 'file', 'need_fill' => true, 'rule' => 'required', 'config' => ['visible_when' => ['field' => 'finance', 'values' => ['later', 'road'], 'required_values' => ['road']]]];
SurveySchema::validateConditions([$parent, $child]);
$answers = SurveySchema::normalize([$parent, $child], [['id' => 1, 'value' => 'later']], true);
$this->assertFalse($answers[1]['need_fill']);
$this->assertFalse($answers[1]['hidden']);
$this->assertNull($answers[1]['value']);
$reply = SurveySchema::replyQuestions([], $answers, ['version' => 2, 'questions' => [$parent, $child]]);
$this->assertFalse($reply[1]['need_fill']);
$answers = SurveySchema::normalize([$parent, $child], [['id' => 1, 'value' => 'no']], true);
$this->assertTrue($answers[1]['hidden']);
$parent['edit_input'] = 'checkbox';
$map = SurveySchema::answerMap([['id' => 1, 'value' => ['later', 'road']]]);
$this->assertTrue(SurveySchema::requiredFor($child, [$parent, $child], $map));
$this->expectException(InvalidArgumentException::class);
SurveySchema::normalize([$parent, $child], [['id' => 1, 'value' => ['road']]], true);
}
public function testRequiredTriggersMustBelongToVisibleTriggers(): void
{
$parent = ['id' => 1, 'name' => '前置', 'field' => 'parent', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '是'], ['key' => 'no', 'value' => '否']]];
$child = ['id' => 2, 'name' => '后续', 'field' => 'child', 'edit_input' => 'text', 'config' => ['visible_when' => ['field' => 'parent', 'values' => ['yes'], 'required_values' => ['no']]]];
$this->expectException(InvalidArgumentException::class);
SurveySchema::validateConditions([$parent, $child]);
}
private function questions(): array
{
return [

Loading…
Cancel
Save