Merge branch 'master' of ssh://47.101.48.251:/data/git/wx.sstbc.com

master
lion 11 hours ago
commit 58931b4971

@ -22,8 +22,10 @@ use App\Models\CourseSign;
use App\Models\CourseType;
use App\Models\Notice;
use App\Models\Order;
use App\Http\Resources\Mobile\PublicCourseResource;
use App\Http\Resources\Mobile\UserListResource;
use App\Models\User;
use App\Models\Upload;
use App\Services\SchoolmateAccessService;
use EasyWeChat\Factory;
use Illuminate\Support\Carbon;
@ -75,18 +77,36 @@ class CourseController extends CommonController
public function course()
{
$all = request()->all();
$list = Course::with('image', 'typeDetail')->withCount('courseSigns')->where(function ($query) use ($all) {
if (isset($all['type'])) {
$query->where('type', $all['type']);
}
if (isset($all['status'])) {
$query->where('status', $all['status']);
}
})->whereIn('sign_status', [10, 40])
$validator = Validator::make($all, [
'page_size' => 'nullable|integer|min:1|max:50',
'page' => 'nullable|integer|min:1',
'type' => 'nullable|integer',
'status' => 'nullable|integer|in:1',
], [
'page_size.max' => '每页最多查询50条课程',
'status.in' => '课程状态参数无效',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$list = Course::with('image', 'typeDetail', 'teacher')
->withCount('courseSigns')
->where('status', 1)
->whereIn('sign_status', [10, 40])
->where('is_virtual', 0)
->where(function ($query) use ($all) {
if (isset($all['type'])) {
$query->where('type', $all['type']);
}
})
->orderBy('sign_status', 'asc')
->orderBy('start_date', 'desc')
->paginate($all['page_size'] ?? 20);
->paginate((int) ($all['page_size'] ?? 20));
$list->setCollection($list->getCollection()->map(function ($course) {
return (new PublicCourseResource($course))->resolve();
}));
return $this->success($list);
}
@ -109,29 +129,30 @@ class CourseController extends CommonController
'course_id.required' => '课程id必填',
];
$validator = Validator::make($all, [
'course_id' => 'required'
'course_id' => 'required|integer|min:1'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = Course::with([
'image',
'qunImage',
'typeDetail',
'courseForms',
'teacher',
'courseContentEvaluation' => function ($query) {
$query->with([
'courseContentEvaluationAsks' => function ($q) {
$q->orderBy('sort', 'asc');
}
]);
}
])->withCount([
'courseSigns as my_user' => function ($query) {
$query->where('user_id', $this->getUserId());
}
])->find($all['course_id']);
return $this->success($detail);
'courseSigns as my_user' => function ($query) {
$query->where('user_id', $this->getUserId());
}
])->where('status', 1)
->whereIn('sign_status', [10, 40])
->where('is_virtual', 0)
->find($all['course_id']);
if (!$detail) {
return response()->json(['message' => '课程不存在'], 404);
}
$this->attachPublicAssets($detail);
return $this->success((new PublicCourseResource($detail))->resolve());
}
/**
@ -186,13 +207,34 @@ class CourseController extends CommonController
'course_id.required' => '课程id必填',
];
$validator = Validator::make($all, [
'course_id' => 'required'
'course_id' => 'required|integer|min:1'
], $messages);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$detail = Course::with('typeDetail', 'courseForms', 'teacher')->find($all['course_id']);
return $this->success($detail);
$detail = Course::with('typeDetail', 'courseForms', 'teacher', 'image', 'qunImage')
->where('status', 1)
->whereIn('sign_status', [10, 40])
->where('is_virtual', 0)
->find($all['course_id']);
if (!$detail) {
return response()->json(['message' => '课程不存在'], 404);
}
$this->attachPublicAssets($detail);
return $this->success((new PublicCourseResource($detail))->resolve());
}
private function attachPublicAssets(Course $course): void
{
$ids = collect($course->publicize_ids ?: [])
->filter(fn ($id) => is_numeric($id) && (int) $id > 0)
->map(fn ($id) => (int) $id)
->values();
$course->setRelation(
'publicAssets',
$ids->isEmpty() ? collect() : Upload::whereIn('id', $ids->all())->get()
);
}
/**

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources\Mobile;
/**
* Explicit internal representation for a future authorized admin API.
* Public mobile endpoints must never use this resource.
*/
class AdminCourseResource extends PublicCourseResource
{
public function toArray($request)
{
$data = parent::toArray($request);
$data['admin_id'] = $this->admin_id;
$data['department_id'] = $this->department_id;
$data['teacher_id'] = $this->teacher_id;
$data['is_virtual'] = (bool) $this->is_virtual;
$data['show_txl'] = (bool) $this->show_txl;
$data['show_mobile'] = (bool) $this->show_mobile;
return $data;
}
}

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Mobile;
/**
* Authenticated mobile representation.
*
* This is intentionally not used by the anonymous endpoints until optional
* mobile authentication is explicitly enabled and tested. It documents the
* boundary for future authenticated course APIs.
*/
class AuthenticatedCourseResource extends PublicCourseResource
{
public function toArray($request)
{
$data = parent::toArray($request);
$data['my_user'] = $this->when(
isset($this->my_user),
(int) $this->my_user
);
return $data;
}
}

@ -0,0 +1,110 @@
<?php
namespace App\Http\Resources\Mobile;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* Public representation of a course.
*
* Do not return Course models directly from public mobile endpoints. Course
* has computed appends and relations that contain internal and personal data.
*/
class PublicCourseResource extends JsonResource
{
public function toArray($request)
{
$course = $this->resource;
return [
'id' => (int) $this->id,
'name' => $this->name,
'start_date' => $this->start_date,
'end_date' => $this->end_date,
'content' => $this->content,
'publicize_content' => $this->publicize_content,
'type' => $this->type === null ? null : (int) $this->type,
'is_fee' => $this->is_fee === null ? null : (int) $this->is_fee,
'total' => $this->total === null ? null : (int) $this->total,
'show_txl' => $this->show_txl === null ? null : (int) $this->show_txl,
'show_mobile' => $this->show_mobile === null ? null : (int) $this->show_mobile,
'url' => $this->url,
'url_title' => $this->url_title,
'status' => (int) $this->status,
'course_status' => (int) $this->course_status,
'sign_status' => (int) $this->sign_status,
'my_user' => isset($this->my_user) ? (int) $this->my_user : 0,
'type_detail' => $this->whenLoaded('typeDetail', function () {
if (!$this->typeDetail) {
return null;
}
return [
'id' => (int) $this->typeDetail->id,
'name' => $this->typeDetail->name,
];
}),
'image' => $this->whenLoaded('image', function () {
if (!$this->image) {
return null;
}
return [
'id' => (int) $this->image->id,
'url' => $this->image->url,
];
}),
'qun_image_id' => $this->qun_image_id === null ? null : (int) $this->qun_image_id,
'qun_image' => $this->whenLoaded('qunImage', function () {
return $this->safeUpload($this->qunImage);
}),
'publicize' => $course->relationLoaded('publicAssets')
? $course->getRelation('publicAssets')->map(fn ($upload) => $this->safeUpload($upload))->values()->all()
: [],
'teacher' => $this->whenLoaded('teacher', function () {
if (!$this->teacher) {
return null;
}
return [
'id' => (int) $this->teacher->id,
'name' => $this->teacher->name,
'introduce' => $this->teacher->introduce,
];
}),
'course_forms' => $this->whenLoaded('courseForms', function () {
return $this->courseForms->map(function ($form) {
// Keep only fields needed to render the public sign-up form.
return [
'id' => (int) $form->id,
'name' => $form->name,
'field' => $form->field,
'edit_input' => $form->edit_input,
'rule' => $form->rule,
'sort' => (int) $form->sort,
'help' => $form->help,
'select_item' => $form->select_item,
'need_fill' => (bool) $form->need_fill,
'belong_user' => (bool) $form->belong_user,
];
})->values()->all();
}),
'course_signs_count' => $this->when(
isset($this->course_signs_count),
(int) $this->course_signs_count
),
];
}
private function safeUpload($upload): ?array
{
if (!$upload) {
return null;
}
return [
'id' => (int) $upload->id,
'url' => $upload->url,
];
}
}

BIN
public/.DS_Store vendored

Binary file not shown.

@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-https://wx.sstbc.com}"
COURSE_ID="${COURSE_ID:-}"
CURL_TIMEOUT="${CURL_TIMEOUT:-20}"
CURL_RESOLVE="${CURL_RESOLVE:-}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
if ! command -v php >/dev/null 2>&1; then
printf 'ERROR: php CLI is required to parse API responses\n' >&2
exit 2
fi
CURL_ARGS=(-sS -L --max-time "$CURL_TIMEOUT")
if [[ "${CURL_INSECURE:-0}" == "1" ]]; then
CURL_ARGS+=(-k)
fi
if [[ -n "$CURL_RESOLVE" ]]; then
CURL_ARGS+=(--resolve "$CURL_RESOLVE")
fi
request() {
local name="$1"
local url="$2"
local body="$TMP_DIR/$name.json"
local status
if ! status="$(curl "${CURL_ARGS[@]}" -o "$body" -w '%{http_code}' "$url")"; then
status="000"
: > "$body"
printf '%s' "$status" > "$TMP_DIR/$name.status"
printf '%-28s HTTP %s REQUEST_FAILED\n' "$name" "$status"
printf '%s\n' "$body"
return 0
fi
printf '%s' "$status" > "$TMP_DIR/$name.status"
printf '%-28s HTTP %s\n' "$name" "$status"
printf '%s\n' "$body"
}
request course-list "$BASE_URL/api/mobile/course/course?page=1&page_size=1"
request course-list-page-size-51 "$BASE_URL/api/mobile/course/course?page=1&page_size=51"
request course-detail-invalid "$BASE_URL/api/mobile/course/course-detail?course_id=not-an-id"
request course-detail-missing "$BASE_URL/api/mobile/course/course-detail?course_id=2147483647"
scan_sensitive_keys() {
local name="$1"
local body="$TMP_DIR/$name.json"
local forbidden
forbidden="$(php -r '
$data = json_decode(file_get_contents($argv[1]), true);
if (!is_array($data)) { exit(2); }
$blocked = ["mobile", "idcard", "id_card", "identity_card", "remark", "admin_id", "department_id", "teacher_id", "deleted_at", "password", "remember_token"];
$found = [];
$walk = function ($value) use (&$walk, &$found, $blocked) {
if (!is_array($value)) { return; }
foreach ($value as $key => $child) {
if (in_array(strtolower((string) $key), $blocked, true)) { $found[(string) $key] = true; }
$walk($child);
}
};
$walk($data);
ksort($found);
echo implode(PHP_EOL, array_keys($found));
' "$body" || true)"
if [[ -n "$forbidden" ]]; then
printf '%s FORBIDDEN_KEYS\n%s\n' "$name" "$forbidden"
exit 1
fi
printf '%s SENSITIVE_SCAN PASS\n' "$name"
}
list_body="$TMP_DIR/course-list.json"
if php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && isset($d["data"]) && is_array($d["data"]) ? 0 : 1);' "$list_body"; then
scan_sensitive_keys course-list
if [[ -z "$COURSE_ID" ]]; then
COURSE_ID="$(php -r '$d=json_decode(file_get_contents($argv[1]), true); echo $d["data"][0]["id"] ?? "";' "$list_body")"
fi
if [[ -n "$COURSE_ID" ]]; then
request course-detail-valid "$BASE_URL/api/mobile/course/course-detail?course_id=$COURSE_ID"
request course-detail-pc-valid "$BASE_URL/api/mobile/course/course-detail-pc?course_id=$COURSE_ID"
scan_sensitive_keys course-detail-valid
scan_sensitive_keys course-detail-pc-valid
php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && isset($d["id"], $d["name"]) ? 0 : 1);' "$TMP_DIR/course-detail-valid.json"
php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && isset($d["id"], $d["name"]) ? 0 : 1);' "$TMP_DIR/course-detail-pc-valid.json"
printf 'DETAIL_CORE_FIELDS PASS\n'
else
printf 'DETAIL_SCAN SKIP: course list is empty\n'
fi
else
printf 'SENSITIVE_SCAN SKIP: course list did not return a data array\n'
fi
if php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && (($d["errcode"] ?? null) === 10001) ? 0 : 1);' "$TMP_DIR/course-list-page-size-51.json"; then
printf 'PAGE_SIZE_VALIDATION PASS\n'
else
printf 'PAGE_SIZE_VALIDATION FAIL\n'
exit 1
fi
if php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && (($d["errcode"] ?? null) === 10001) ? 0 : 1);' "$TMP_DIR/course-detail-invalid.json"; then
printf 'COURSE_ID_VALIDATION PASS\n'
else
printf 'COURSE_ID_VALIDATION FAIL\n'
exit 1
fi
if [[ "$(cat "$TMP_DIR/course-detail-missing.status")" == "404" ]] || php -r '$d=json_decode(file_get_contents($argv[1]), true); exit(is_array($d) && (($d["message"] ?? null) === "课程不存在") ? 0 : 1);' "$TMP_DIR/course-detail-missing.json"; then
printf 'MISSING_COURSE_VALIDATION PASS\n'
else
printf 'MISSING_COURSE_VALIDATION FAIL\n'
exit 1
fi

@ -0,0 +1,40 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class MobileCourseEndpointValidationTest extends TestCase
{
public function test_course_list_rejects_page_size_above_public_limit(): void
{
$response = $this->getJson('/api/mobile/course/course?page_size=51');
$response->assertOk()
->assertJsonPath('errcode', 10001);
}
public function test_course_list_rejects_non_public_status_filter(): void
{
$response = $this->getJson('/api/mobile/course/course?status=0');
$response->assertOk()
->assertJsonPath('errcode', 10001);
}
public function test_course_detail_rejects_non_integer_course_id(): void
{
$response = $this->getJson('/api/mobile/course/course-detail?course_id=not-an-id');
$response->assertOk()
->assertJsonPath('errcode', 10001);
}
public function test_pc_course_detail_rejects_non_integer_course_id(): void
{
$response = $this->getJson('/api/mobile/course/course-detail-pc?course_id=not-an-id');
$response->assertOk()
->assertJsonPath('errcode', 10001);
}
}

@ -0,0 +1,292 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\User;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class MobileCoursePublicDataTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.default' => 'sqlite',
'database.connections.sqlite.database' => ':memory:',
]);
DB::purge('sqlite');
$this->createSchema();
$this->seedCourseData();
}
protected function tearDown(): void
{
Auth::guard('mobile')->forgetUser();
Auth::guard('admin')->forgetUser();
DB::disconnect('sqlite');
parent::tearDown();
}
public function test_anonymous_user_sees_only_published_public_courses(): void
{
$response = $this->getJson('/api/mobile/course/course');
$response->assertOk();
$data = $response->json('data');
$this->assertNoSensitiveKeys($response->json());
$this->assertCount(1, $data);
$this->assertSame(150, $data[0]['id']);
$this->assertSame('张老师', $data[0]['teacher']['name']);
$this->assertArrayNotHasKey('mobile', $data[0]['teacher']);
$this->assertArrayNotHasKey('admin_id', $data[0]);
}
public function test_regular_mobile_user_cannot_read_unpublished_course_details(): void
{
$user = new User();
$user->forceFill(['id' => 200]);
Auth::guard('mobile')->setUser($user);
$response = $this->getJson('/api/mobile/course/course-detail?course_id=151');
$response->assertNotFound();
}
public function test_registered_mobile_user_still_receives_safe_course_detail(): void
{
$user = new User();
$user->forceFill(['id' => 201]);
Auth::guard('mobile')->setUser($user);
DB::table('course_signs')->insert([
'id' => 1,
'course_id' => 150,
'user_id' => 201,
]);
$response = $this->getJson('/api/mobile/course/course-detail?course_id=150');
$response->assertOk();
$this->assertNoSensitiveKeys($response->json());
$this->assertSame('公开课程', $response->json('name'));
$this->assertSame('张老师', $response->json('teacher.name'));
$this->assertStringEndsWith('/course/public.jpg', $response->json('publicize.0.url'));
$this->assertStringEndsWith('/course/group.jpg', $response->json('qun_image.url'));
$this->assertArrayNotHasKey('folder', $response->json('publicize.0'));
$this->assertArrayNotHasKey('name', $response->json('qun_image'));
$this->assertArrayNotHasKey('mobile', $response->json('teacher'));
$this->assertArrayNotHasKey('admin_id', $response->json());
$this->assertArrayNotHasKey('department_id', $response->json());
$this->assertArrayNotHasKey('deleted_at', $response->json());
$this->assertArrayNotHasKey('admin_id', $response->json('course_forms.0'));
}
public function test_admin_user_does_not_expand_public_mobile_response(): void
{
$admin = new Admin();
$admin->forceFill(['id' => 1]);
Auth::guard('admin')->setUser($admin);
$response = $this->getJson('/api/mobile/course/course-detail-pc?course_id=150');
$response->assertOk();
$this->assertNoSensitiveKeys($response->json());
$this->assertArrayNotHasKey('mobile', $response->json('teacher'));
$this->assertArrayNotHasKey('admin_id', $response->json());
$this->assertArrayNotHasKey('teacher_id', $response->json());
}
public function test_deleted_virtual_and_not_started_courses_are_not_publicly_addressable(): void
{
foreach ([152, 153, 154] as $courseId) {
$response = $this->getJson('/api/mobile/course/course-detail?course_id=' . $courseId);
$response->assertNotFound();
}
}
private function createSchema(): void
{
Schema::create('courses', function (Blueprint $table) {
$table->increments('id');
$table->integer('admin_id')->nullable();
$table->integer('department_id')->nullable();
$table->string('name')->nullable();
$table->integer('image_id')->nullable();
$table->integer('qun_image_id')->nullable();
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->integer('type')->nullable();
$table->text('content')->nullable();
$table->text('publicize_content')->nullable();
$table->json('publicize_ids')->nullable();
$table->tinyInteger('is_fee')->nullable();
$table->integer('total')->nullable();
$table->string('url')->nullable();
$table->string('url_title')->nullable();
$table->integer('teacher_id')->nullable();
$table->tinyInteger('status')->default(0);
$table->tinyInteger('course_status')->default(0);
$table->tinyInteger('sign_status')->default(0);
$table->boolean('is_virtual')->default(0);
$table->boolean('show_txl')->default(1);
$table->boolean('show_mobile')->default(1);
$table->timestamps();
$table->softDeletes();
});
Schema::create('teachers', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('mobile')->nullable();
$table->text('introduce')->nullable();
$table->string('remark')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('course_types', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('wait_tip')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('course_forms', function (Blueprint $table) {
$table->increments('id');
$table->integer('course_id')->nullable();
$table->integer('admin_id')->nullable();
$table->integer('department_id')->nullable();
$table->string('name')->nullable();
$table->string('field')->nullable();
$table->string('edit_input')->nullable();
$table->string('rule')->nullable();
$table->integer('sort')->default(1);
$table->string('help')->nullable();
$table->text('select_item')->nullable();
$table->boolean('need_fill')->default(0);
$table->boolean('belong_user')->default(0);
$table->timestamps();
$table->softDeletes();
});
Schema::create('uploads', function (Blueprint $table) {
$table->increments('id');
$table->string('folder')->nullable();
$table->string('name')->nullable();
$table->string('original_name')->nullable();
$table->timestamps();
});
DB::table('uploads')->insert([
['id' => 21, 'folder' => 'course', 'name' => 'public.jpg'],
['id' => 22, 'folder' => 'course', 'name' => 'group.jpg'],
]);
Schema::create('course_signs', function (Blueprint $table) {
$table->increments('id');
$table->integer('course_id')->nullable();
$table->integer('user_id')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('course_content_evaluations', function (Blueprint $table) {
$table->increments('id');
$table->integer('course_id')->nullable();
$table->string('course_content_id')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('course_content_evaluation_asks', function (Blueprint $table) {
$table->increments('id');
$table->integer('course_content_evaluation_id')->nullable();
$table->integer('course_content_id')->nullable();
$table->integer('sort')->default(1);
$table->timestamps();
$table->softDeletes();
});
}
private function seedCourseData(): void
{
DB::table('teachers')->insert([
'id' => 11,
'name' => '张老师',
'mobile' => '13800138000',
'introduce' => '公开介绍',
'remark' => '内部备注',
]);
DB::table('course_types')->insert(['id' => 3, 'name' => '管理类']);
DB::table('course_forms')->insert([
'id' => 1,
'course_id' => 150,
'admin_id' => 9,
'department_id' => 10,
'name' => '姓名',
'field' => 'name',
'edit_input' => 'input',
'rule' => 'required',
'sort' => 1,
'help' => '请填写姓名',
'need_fill' => 1,
'belong_user' => 1,
]);
$base = [
'name' => '公开课程',
'start_date' => '2026-08-01',
'end_date' => '2026-08-02',
'type' => 3,
'content' => '公开介绍',
'publicize_content' => '宣传介绍',
'teacher_id' => 11,
'status' => 1,
'course_status' => 20,
'sign_status' => 10,
'is_virtual' => 0,
'show_txl' => 1,
'show_mobile' => 1,
'is_fee' => 0,
'total' => 30,
'url' => 'https://example.test/review',
'url_title' => '精彩回顾',
'publicize_ids' => json_encode([21]),
'qun_image_id' => 22,
'deleted_at' => null,
];
DB::table('courses')->insert([
array_merge($base, ['id' => 150]),
array_merge($base, ['id' => 151, 'status' => 0]),
array_merge($base, ['id' => 152, 'is_virtual' => 1]),
array_merge($base, ['id' => 153, 'sign_status' => 30]),
array_merge($base, ['id' => 154, 'deleted_at' => now()]),
]);
}
private function assertNoSensitiveKeys($value): void
{
$forbidden = [
'mobile', 'idcard', 'id_card', 'identity_card', 'remark',
'admin_id', 'department_id', 'teacher_id', 'deleted_at',
'password', 'remember_token',
];
if (is_array($value)) {
foreach ($value as $key => $item) {
$this->assertNotContains(strtolower((string) $key), $forbidden);
$this->assertNoSensitiveKeys($item);
}
}
}
}

@ -0,0 +1,99 @@
<?php
namespace Tests\Unit\Http\Resources\Mobile;
use App\Http\Resources\Mobile\PublicCourseResource;
use App\Models\Course;
use App\Models\CourseForm;
use App\Models\CourseType;
use App\Models\Teacher;
use Illuminate\Support\Collection;
use Tests\TestCase;
class PublicCourseResourceTest extends TestCase
{
public function test_public_resource_returns_only_allowlisted_course_fields(): void
{
$course = new Course();
$course->setRawAttributes([
'id' => 150,
'name' => '公开课程',
'start_date' => '2026-08-01',
'end_date' => '2026-08-02',
'content' => '公开介绍',
'publicize_content' => '宣传介绍',
'status' => 1,
'course_status' => 20,
'sign_status' => 10,
'admin_id' => 9,
'department_id' => 10,
'teacher_id' => 11,
'show_mobile' => 1,
]);
$teacher = new Teacher();
$teacher->setRawAttributes([
'id' => 11,
'name' => '张老师',
'mobile' => '13800138000',
'remark' => '内部备注',
]);
$form = new CourseForm();
$form->setRawAttributes([
'id' => 1,
'name' => '姓名',
'field' => 'name',
'edit_input' => 'input',
'rule' => 'required',
'sort' => 1,
'help' => '请填写姓名',
'select_item' => null,
'need_fill' => 1,
'belong_user' => 1,
'admin_id' => 9,
'department_id' => 10,
]);
$type = new CourseType();
$type->setRawAttributes(['id' => 3, 'name' => '管理类', 'wait_tip' => '内部提示']);
$course->setRelation('teacher', $teacher);
$course->setRelation('courseForms', new Collection([$form]));
$course->setRelation('typeDetail', $type);
$payload = (new PublicCourseResource($course))->resolve();
$this->assertSame(150, $payload['id']);
$this->assertSame('公开课程', $payload['name']);
$this->assertSame('张老师', $payload['teacher']['name']);
$this->assertArrayNotHasKey('mobile', $payload['teacher']);
$this->assertArrayNotHasKey('remark', $payload['teacher']);
$this->assertArrayNotHasKey('admin_id', $payload);
$this->assertArrayNotHasKey('department_id', $payload);
$this->assertArrayNotHasKey('teacher_id', $payload);
$this->assertArrayNotHasKey('wait_tip', $payload['type_detail']);
$this->assertArrayNotHasKey('admin_id', $payload['course_forms'][0]);
$this->assertArrayNotHasKey('department_id', $payload['course_forms'][0]);
$this->assertArrayNotHasKey('deleted_at', $payload['course_forms'][0]);
}
public function test_public_resource_does_not_include_model_appends(): void
{
$course = new Course();
$course->setRawAttributes([
'id' => 1,
'name' => '公开课程',
'status' => 1,
'course_status' => 20,
'sign_status' => 10,
]);
$payload = (new PublicCourseResource($course))->resolve();
$this->assertArrayNotHasKey('teacher_detail', $payload);
$this->assertArrayNotHasKey('show_mobile_text', $payload);
$this->assertSame([], $payload['publicize']);
$this->assertArrayNotHasKey('qrcode', $payload);
}
}
Loading…
Cancel
Save