Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
e926cd5c89
  1. 256
      admin/src/app/views/auth/components/edit-role.vue
  2. 17
      admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue
  3. 172
      niucloud/app/adminapi/controller/sys/SysMenu.php
  4. 55
      niucloud/app/adminapi/controller/uniapp/UniappAuth.php
  5. 50
      niucloud/app/adminapi/route/sys_menu.php
  6. 32
      niucloud/app/adminapi/route/uniapp_auth.php
  7. 272
      niucloud/app/job/transfer/schedule/CourseValidityJob.php
  8. 277
      niucloud/app/job/transfer/schedule/StaffAttendanceJob.php
  9. 372
      niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php
  10. 310
      niucloud/app/job/transfer/schedule/TeachingServiceJob.php
  11. 14
      niucloud/app/model/sys/SysMenu.php
  12. 26
      niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php
  13. 231
      niucloud/app/service/admin/sys/SysMenuService.php
  14. 106
      niucloud/app/service/admin/uniapp/UniappAuthService.php
  15. 31
      niucloud/app/service/api/apiService/ResourceSharingService.php
  16. 89
      niucloud/app/service/api/login/UnifiedLoginService.php
  17. 2
      uniapp/pages-market/clue/class_arrangement.vue
  18. 4
      uniapp/pages-market/clue/index.vue
  19. 69
      uniapp/pages/common/home/index.vue
  20. 69
      uniapp/test-edit-clues.html
  21. 268
      uniapp/学员端订单接口问题修复报告.md

256
admin/src/app/views/auth/components/edit-role.vue

@ -62,29 +62,61 @@
</el-form-item> </el-form-item>
<el-form-item :label="t('permission')" prop="rules"> <el-form-item :label="t('permission')" prop="rules">
<div class="flex items-center justify-between w-11/12"> <el-tabs v-model="activePermissionTab" class="w-full">
<div> <!-- PC端权限 -->
<el-checkbox v-model="selectAll" :label="t('selectAll')"/> <el-tab-pane label="PC端权限" name="pc">
<el-checkbox v-model="checkStrictly" :label="t('checkStrictly')"/> <div class="flex items-center justify-between w-11/12">
</div> <div>
<el-button link type="primary" @click="menuAction()">{{ <el-checkbox v-model="selectAllPC" :label="t('selectAll')"/>
t('foldText') <el-checkbox v-model="checkStrictlyPC" :label="t('checkStrictly')"/>
}} </div>
</el-button> <el-button link type="primary" @click="menuActionPC()">{{
</div> t('foldText')
<el-scrollbar height="35vh" class="w-full"> }}
<el-tree </el-button>
:data="menus" </div>
:props="{ label: 'menu_name' }" <el-scrollbar height="35vh" class="w-full">
:default-checked-keys="formData.rules" <el-tree
:check-strictly="checkStrictly" :data="menus"
show-checkbox :props="{ label: 'menu_name' }"
default-expand-all :default-checked-keys="formData.rules"
@check-change="handleCheckChange" :check-strictly="checkStrictlyPC"
node-key="menu_key" show-checkbox
ref="treeRef" default-expand-all
/> @check-change="handleCheckChangePC"
</el-scrollbar> node-key="menu_key"
ref="treeRefPC"
/>
</el-scrollbar>
</el-tab-pane>
<!-- 移动端权限 -->
<el-tab-pane label="移动端权限" name="mobile">
<div class="flex items-center justify-between w-11/12">
<div>
<el-checkbox v-model="selectAllMobile" :label="t('selectAll')"/>
<el-checkbox v-model="checkStrictlyMobile" :label="t('checkStrictly')"/>
</div>
<el-button link type="primary" @click="menuActionMobile()">{{
t('foldText')
}}
</el-button>
</div>
<el-scrollbar height="35vh" class="w-full">
<el-tree
:data="mobileMenus"
:props="{ label: 'menu_name' }"
:default-checked-keys="formData.mobile_rules"
:check-strictly="checkStrictlyMobile"
show-checkbox
default-expand-all
@check-change="handleCheckChangeMobile"
node-key="menu_key"
ref="treeRefMobile"
/>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -121,19 +153,64 @@ let popTitle: string = ''
// //
const menus = ref<Record<string, any>[]>([]) const menus = ref<Record<string, any>[]>([])
const mobileMenus = ref<Record<string, any>[]>([])
// PC
getAuthMenus({is_button: 0}).then((res) => { getAuthMenus({is_button: 0}).then((res) => {
menus.value = res.data menus.value = res.data
}) })
// // UniApp
const selectAll = ref(false) const getMobileMenus = async () => {
const checkStrictly = ref(false) try {
const treeRef: Record<string, any> | null = ref(null) // 使UniApp
watch(selectAll, () => { const response = await fetch('http://localhost:20080/adminapi/uniapp_auth/menus', {
if (selectAll.value) { headers: {
treeRef.value.setCheckedNodes(toRaw(menus.value)) 'Content-Type': 'application/json',
'token': localStorage.getItem('token') || ''
}
})
const result = await response.json()
if (result.code === 1) {
mobileMenus.value = result.data.map(item => ({
menu_key: item.menu_key,
menu_name: item.menu_name,
id: item.id,
children: []
}))
}
} catch (error) {
console.error('获取移动端菜单失败:', error)
//
mobileMenus.value = []
}
}
getMobileMenus()
//
const activePermissionTab = ref('pc')
// PC
const selectAllPC = ref(false)
const checkStrictlyPC = ref(false)
const treeRefPC: Record<string, any> | null = ref(null)
watch(selectAllPC, () => {
if (selectAllPC.value) {
treeRefPC.value.setCheckedNodes(toRaw(menus.value))
} else {
treeRefPC.value.setCheckedNodes([])
}
})
//
const selectAllMobile = ref(false)
const checkStrictlyMobile = ref(false)
const treeRefMobile: Record<string, any> | null = ref(null)
watch(selectAllMobile, () => {
if (selectAllMobile.value) {
treeRefMobile.value.setCheckedNodes(toRaw(mobileMenus.value))
} else { } else {
treeRef.value.setCheckedNodes([]) treeRefMobile.value.setCheckedNodes([])
} }
}) })
@ -151,34 +228,67 @@ const getRolekeyDictList = async () => {
} }
getRolekeyDictList() getRolekeyDictList()
const handleCheckChange = debounce((e) => { // PC
formData.rules = treeRef.value.getCheckedKeys() const handleCheckChangePC = debounce((e) => {
formData.rules = treeRefPC.value.getCheckedKeys()
}) })
const menuAction = () => { const menuActionPC = () => {
if (isOpen.value) { if (isOpen.value) {
collapseAll(menus.value) collapseAllPC(menus.value)
isOpen.value = false isOpen.value = false
} else { } else {
unFoldAll(menus.value) unFoldAllPC(menus.value)
isOpen.value = true isOpen.value = true
} }
} }
// //
const unFoldAll = (data: any) => { const handleCheckChangeMobile = debounce((e) => {
formData.mobile_rules = treeRefMobile.value.getCheckedKeys()
})
const menuActionMobile = () => {
if (isOpen.value) {
collapseAllMobile(mobileMenus.value)
isOpen.value = false
} else {
unFoldAllMobile(mobileMenus.value)
isOpen.value = true
}
}
// PC
const unFoldAllPC = (data: any) => {
Object.keys(data).forEach((key: string | any) => { Object.keys(data).forEach((key: string | any) => {
treeRef.value.store.nodesMap[data[key].menu_key].expanded = true treeRefPC.value.store.nodesMap[data[key].menu_key].expanded = true
if (data[key].children && data[key].children.length > 0) if (data[key].children && data[key].children.length > 0)
collapseAll(data[key].children) unFoldAllPC(data[key].children)
}) })
} }
// // PC
const collapseAll = (data: any) => { const collapseAllPC = (data: any) => {
Object.keys(data).forEach((key: string | any) => { Object.keys(data).forEach((key: string | any) => {
treeRef.value.store.nodesMap[data[key].menu_key].expanded = false treeRefPC.value.store.nodesMap[data[key].menu_key].expanded = false
if (data[key].children && data[key].children.length > 0) if (data[key].children && data[key].children.length > 0)
collapseAll(data[key].children) collapseAllPC(data[key].children)
})
}
//
const unFoldAllMobile = (data: any) => {
Object.keys(data).forEach((key: string | any) => {
treeRefMobile.value.store.nodesMap[data[key].menu_key].expanded = true
if (data[key].children && data[key].children.length > 0)
unFoldAllMobile(data[key].children)
})
}
//
const collapseAllMobile = (data: any) => {
Object.keys(data).forEach((key: string | any) => {
treeRefMobile.value.store.nodesMap[data[key].menu_key].expanded = false
if (data[key].children && data[key].children.length > 0)
collapseAllMobile(data[key].children)
}) })
} }
/** /**
@ -191,6 +301,7 @@ const initialFormData = {
role_key: '', role_key: '',
dept_id:'', dept_id:'',
rules: [], rules: [],
mobile_rules: [], //
} }
const formData: Record<string, any> = reactive({...initialFormData}) const formData: Record<string, any> = reactive({...initialFormData})
@ -229,7 +340,36 @@ const confirm = async (formEl: FormInstance | undefined) => {
loading.value = true loading.value = true
const data = Object.assign({}, formData) const data = Object.assign({}, formData)
data.rules = data.rules.concat(treeRef.value.getHalfCheckedKeys())
// PC
data.rules = data.rules.concat(treeRefPC.value.getHalfCheckedKeys())
//
if (treeRefMobile.value) {
data.mobile_rules = data.mobile_rules.concat(treeRefMobile.value.getHalfCheckedKeys())
// UniAppAPI
if (data.role_id && data.mobile_rules.length >= 0) {
try {
const mobileResponse = await fetch(`http://localhost:20080/adminapi/uniapp_auth/set_role_menus/${data.role_id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': localStorage.getItem('token') || ''
},
body: JSON.stringify({
menu_ids: data.mobile_rules
})
})
const mobileResult = await mobileResponse.json()
if (mobileResult.code !== 1) {
console.error('移动端权限设置失败:', mobileResult.msg)
}
} catch (error) {
console.error('设置移动端权限失败:', error)
}
}
}
save(data) save(data)
.then((res) => { .then((res) => {
@ -248,12 +388,31 @@ const confirm = async (formEl: FormInstance | undefined) => {
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
selectAll.value = false selectAllPC.value = false
selectAllMobile.value = false
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
popTitle = t('addRole') popTitle = t('addRole')
if (row) { if (row) {
popTitle = t('updateRole') popTitle = t('updateRole')
const data = await (await getRoleInfo(row.role_id)).data const data = await (await getRoleInfo(row.role_id)).data
//
let mobileRulesData = []
try {
const mobileResponse = await fetch(`http://localhost:20080/adminapi/uniapp_auth/role_menus/${row.role_id}`, {
headers: {
'Content-Type': 'application/json',
'token': localStorage.getItem('token') || ''
}
})
const mobileResult = await mobileResponse.json()
if (mobileResult.code === 1) {
mobileRulesData = mobileResult.data.map(item => item.menu_key)
}
} catch (error) {
console.error('获取移动端权限失败:', error)
}
Object.keys(formData).forEach((key: string) => { Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) { if (data[key] != undefined) {
if (key == 'rules') { if (key == 'rules') {
@ -264,11 +423,16 @@ const setFormData = async (row: any = null) => {
checked(data.rules[i], menus.value, newArr) checked(data.rules[i], menus.value, newArr)
}) })
formData[key] = newArr formData[key] = newArr
} else if (key == 'mobile_rules') {
formData[key] = mobileRulesData
} else { } else {
formData[key] = data[key] formData[key] = data[key]
} }
} }
}) })
//
formData.mobile_rules = mobileRulesData
} }
loading.value = false loading.value = false
} }

17
admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue

@ -234,22 +234,31 @@ const setFormData = async (row: any = null) => {
person_id.value = ''; person_id.value = '';
console.log(person_id.value) console.log(person_id.value)
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
if (row.dept_id) { if (row && row.dept_id) {
formData.dept_id = parseInt(row.dept_id) formData.dept_id = parseInt(row.dept_id)
} }
if (row.person_id) { if (row && row.person_id) {
formData.person_id = parseInt(row.person_id) formData.person_id = parseInt(row.person_id)
person_id.value = parseInt(row.person_id) person_id.value = parseInt(row.person_id)
} }
loading.value = true loading.value = true
if (row.id) { if (row && row.id) {
const data = await (await getCampusPersonRoleInfo(row.id)).data const data = await (await getCampusPersonRoleInfo(row.id)).data
if (data) if (data)
Object.keys(formData).forEach((key: string) => { Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) {
// 0""
if (key === 'campus_id' && data[key] === 0) {
formData[key] = ''
} else {
formData[key] = data[key]
}
}
}) })
} }
//
await setRoleIdList()
loading.value = false loading.value = false
} }

172
niucloud/app/adminapi/controller/sys/SysMenu.php

@ -0,0 +1,172 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\adminapi\controller\sys;
use app\service\admin\sys\SysMenuService;
use core\base\BaseAdminController;
use think\Response;
/**
* 系统菜单管理控制器
*/
class SysMenu extends BaseAdminController
{
/**
* 获取菜单列表
* @return Response
*/
public function lists()
{
$data = $this->request->params([
['keyword', ''],
['status', ''],
['page', 1],
['limit', 20]
]);
$service = new SysMenuService();
return success($service->getPage($data));
}
/**
* 获取菜单详情
* @param int $id
* @return Response
*/
public function info(int $id)
{
$service = new SysMenuService();
return success($service->getInfo($id));
}
/**
* 添加菜单
* @return Response
*/
public function add()
{
$data = $this->request->params([
['menu_key', ''],
['menu_name', ''],
['menu_icon', ''],
['menu_path', ''],
['menu_params', ''],
['sort_order', 0],
['status', 1],
['description', '']
]);
$this->validate($data, [
'menu_key' => 'require|unique:sys_menus',
'menu_name' => 'require',
'menu_path' => 'require'
]);
$service = new SysMenuService();
$id = $service->add($data);
return success(['id' => $id], '添加成功');
}
/**
* 编辑菜单
* @param int $id
* @return Response
*/
public function edit(int $id)
{
$data = $this->request->params([
['menu_key', ''],
['menu_name', ''],
['menu_icon', ''],
['menu_path', ''],
['menu_params', ''],
['sort_order', 0],
['status', 1],
['description', '']
]);
$this->validate($data, [
'menu_key' => 'require',
'menu_name' => 'require',
'menu_path' => 'require'
]);
$service = new SysMenuService();
$service->edit($id, $data);
return success([], '编辑成功');
}
/**
* 删除菜单
* @param int $id
* @return Response
*/
public function del(int $id)
{
$service = new SysMenuService();
$service->del($id);
return success([], '删除成功');
}
/**
* 修改菜单状态
* @param int $id
* @return Response
*/
public function modifyStatus(int $id)
{
$data = $this->request->params([
['status', 1]
]);
$service = new SysMenuService();
$service->modifyStatus($id, $data['status']);
return success([], '操作成功');
}
/**
* 获取所有菜单(用于角色权限配置)
* @return Response
*/
public function getAllMenus()
{
$service = new SysMenuService();
return success($service->getAllMenus());
}
/**
* 获取角色菜单权限
* @param int $roleId
* @return Response
*/
public function getRoleMenus(int $roleId)
{
$service = new SysMenuService();
return success($service->getRoleMenus($roleId));
}
/**
* 设置角色菜单权限
* @param int $roleId
* @return Response
*/
public function setRoleMenus(int $roleId)
{
$data = $this->request->params([
['menu_ids', []]
]);
$service = new SysMenuService();
$service->setRoleMenus($roleId, $data['menu_ids']);
return success([], '设置成功');
}
}

55
niucloud/app/adminapi/controller/uniapp/UniappAuth.php

@ -0,0 +1,55 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\adminapi\controller\uniapp;
use core\base\BaseAdminController;
use app\service\admin\uniapp\UniappAuthService;
/**
* UniApp权限管理控制器
* Class UniappAuth
* @package app\adminapi\controller\uniapp
*/
class UniappAuth extends BaseAdminController
{
/**
* 获取所有UniApp菜单(用于权限配置)
*/
public function getMenus()
{
return success((new UniappAuthService())->getAllMenus());
}
/**
* 获取角色的UniApp菜单权限
* @param int $role_id
* @return \think\Response
*/
public function getRoleMenus(int $role_id)
{
return success((new UniappAuthService())->getRoleMenus($role_id));
}
/**
* 设置角色的UniApp菜单权限
* @param int $role_id
* @return \think\Response
*/
public function setRoleMenus(int $role_id)
{
$data = $this->request->params([
['menu_ids', []]
]);
(new UniappAuthService())->setRoleMenus($role_id, $data['menu_ids']);
return success([], '设置成功');
}
}

50
niucloud/app/adminapi/route/sys_menu.php

@ -0,0 +1,50 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
use think\facade\Route;
/**
* 系统菜单管理路由
*/
Route::group('sys_menu', function () {
// 菜单列表
Route::get('lists', 'app\adminapi\controller\sys\SysMenu@lists');
// 菜单详情
Route::get('info/:id', 'app\adminapi\controller\sys\SysMenu@info');
// 添加菜单
Route::post('add', 'app\adminapi\controller\sys\SysMenu@add');
// 编辑菜单
Route::put('edit/:id', 'app\adminapi\controller\sys\SysMenu@edit');
// 删除菜单
Route::delete('del/:id', 'app\adminapi\controller\sys\SysMenu@del');
// 修改菜单状态
Route::put('modify_status/:id', 'app\adminapi\controller\sys\SysMenu@modifyStatus');
// 获取所有菜单(用于角色权限配置)
Route::get('all_menus', 'app\adminapi\controller\sys\SysMenu@getAllMenus');
// 获取角色菜单权限
Route::get('role_menus/:role_id', 'app\adminapi\controller\sys\SysMenu@getRoleMenus');
// 设置角色菜单权限
Route::post('set_role_menus/:role_id', 'app\adminapi\controller\sys\SysMenu@setRoleMenus');
})->middleware([
app\adminapi\middleware\AdminCheckToken::class,
app\adminapi\middleware\AdminCheckRole::class,
app\adminapi\middleware\AdminLog::class
]);

32
niucloud/app/adminapi/route/uniapp_auth.php

@ -0,0 +1,32 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
use think\facade\Route;
/**
* UniApp权限管理路由
*/
Route::group('uniapp_auth', function () {
// 获取所有UniApp菜单(用于权限配置)
Route::get('menus', 'app\adminapi\controller\uniapp\UniappAuth@getMenus');
// 获取角色的UniApp菜单权限
Route::get('role_menus/:role_id', 'app\adminapi\controller\uniapp\UniappAuth@getRoleMenus');
// 设置角色的UniApp菜单权限
Route::post('set_role_menus/:role_id', 'app\adminapi\controller\uniapp\UniappAuth@setRoleMenus');
})->middleware([
app\adminapi\middleware\AdminCheckToken::class,
app\adminapi\middleware\AdminCheckRole::class,
app\adminapi\middleware\AdminLog::class
]);

272
niucloud/app/job/transfer/schedule/CourseValidityJob.php

@ -0,0 +1,272 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\job\transfer\schedule;
use app\model\course\Course;
use app\model\course_booking\CourseBooking;
use think\facade\Db;
use core\base\BaseJob;
use think\facade\Log;
/**
* 定时处理课程有效状态任务
* 检查课程的有效期,更新过期课程状态
*/
class CourseValidityJob extends BaseJob
{
/**
* 执行定时任务
* @param mixed ...$data 任务参数
* @return bool 处理结果
*/
public function doJob(...$data)
{
Log::write('开始执行课程有效状态检查任务');
try {
// 处理过期课程
$expiredResult = $this->processExpiredCourses();
// 处理孩子的固定课
$warningResult = $this->processStudentCourses();
Log::write('课程有效状态检查任务执行完成-' . print_r([
'expired_courses' => $expiredResult['count'],
'warning_courses' => $warningResult['count'],
], true));
return true;
} catch (\Exception $e) {
Log::write('课程有效状态检查任务执行失败:' . $e->getMessage());
throw $e;
}
}
/**
* 处理课程的状态
* 根据school_course_schedule表的course_date和time_slot更新课程状态
* @return array 处理结果
*/
private function processExpiredCourses()
{
$currentDate = date('Y-m-d');
$currentTime = date('H:i:s');
$currentDateTime = date('Y-m-d H:i:s');
$count = 0;
try {
// 1. 更新已结束的课程 - course_date小于当前日期
$completedCount = Db::name('school_course_schedule')
->where('course_date', '<', $currentDate)
->where('status', '<>', 'completed')
->update([
'status' => 'completed',
'updated_at' => $currentDateTime
]);
Log::write("更新已结束课程数量: {$completedCount}");
$count += $completedCount;
// 2. 处理今天的课程,根据time_slot判断状态
$todaySchedules = Db::name('school_course_schedule')
->where('course_date', $currentDate)
->where('status', '<>', 'completed')
->select();
foreach ($todaySchedules as $schedule) {
$newStatus = $this->calculateCourseStatus($schedule['time_slot'], $currentTime);
if ($newStatus && $newStatus != $schedule['status']) {
Db::name('school_course_schedule')
->where('id', $schedule['id'])
->update([
'status' => $newStatus,
'updated_at' => $currentDateTime
]);
$count++;
Log::write("更新课程状态: ID {$schedule['id']}, 时间段 {$schedule['time_slot']}, 状态 {$schedule['status']} -> {$newStatus}");
}
}
} catch (\Exception $e) {
Log::write('处理课程状态失败: ' . $e->getMessage());
throw $e;
}
return ['count' => $count];
}
/**
* 处理学员固定课的安排
* 查看school_person_course_schedule表中schedule_type=2且person_type=student的课程
* 自动为下周相同时间新增上课记录
* @return array 处理结果
*/
private function processStudentCourses()
{
$currentDate = date('Y-m-d');
$currentDateTime = date('Y-m-d H:i:s');
$count = 0;
try {
// 查找学员的固定课程安排
$studentFixedCourses = Db::name('school_person_course_schedule')
->where('schedule_type', 2)
->where('person_type', 'student')
->where('status', 1) // 假设1为有效状态
->select();
foreach ($studentFixedCourses as $studentCourse) {
// 通过schedule_id获取课程安排信息
$courseSchedule = Db::name('school_course_schedule')
->where('id', $studentCourse['schedule_id'])
->find();
if (!$courseSchedule) {
continue;
}
// 计算下周同一天的日期
$courseDate = $courseSchedule['course_date'];
$dayOfWeek = date('w', strtotime($courseDate)); // 0(周日)到6(周六)
$nextWeekDate = $this->getNextWeekSameDay($courseDate);
// 检查下周那天是否有相同时间段的课程安排
$nextWeekSchedule = Db::name('school_course_schedule')
->where('course_date', $nextWeekDate)
->where('time_slot', $courseSchedule['time_slot'])
->where('venue_id', $courseSchedule['venue_id']) // 同一场地
->find();
if ($nextWeekSchedule) {
// 检查该学员是否已经有下周的上课记录
$existingRecord = Db::name('school_person_course_schedule')
->where('person_id', $studentCourse['person_id'])
->where('schedule_id', $nextWeekSchedule['id'])
->where('person_type', 'student')
->count();
if ($existingRecord == 0) {
// 为学员新增下周的上课记录
$newRecord = [
'person_id' => $studentCourse['person_id'],
'schedule_id' => $nextWeekSchedule['id'],
'person_type' => 'student',
'schedule_type' => 2, // 固定课
'status' => 1,
'created_at' => $currentDateTime,
'updated_at' => $currentDateTime
];
// 复制其他可能的字段
if (isset($studentCourse['course_id'])) {
$newRecord['course_id'] = $studentCourse['course_id'];
}
if (isset($studentCourse['venue_id'])) {
$newRecord['venue_id'] = $studentCourse['venue_id'];
}
Db::name('school_person_course_schedule')->insert($newRecord);
$count++;
Log::write("为学员新增下周固定课: 学员ID {$studentCourse['person_id']}, 日期 {$nextWeekDate}, 时间段 {$courseSchedule['time_slot']}");
}
} else {
Log::write("下周无对应课程安排,跳过: 日期 {$nextWeekDate}, 时间段 {$courseSchedule['time_slot']}");
}
}
} catch (\Exception $e) {
Log::write('处理学员固定课安排失败: ' . $e->getMessage());
throw $e;
}
return ['count' => $count];
}
/**
* 根据时间段和当前时间计算课程状态
* @param string $timeSlot 时间段,格式如 "11:30-12:30"
* @param string $currentTime 当前时间,格式如 "14:30:00"
* @return string|null 课程状态:upcoming, ongoing, completed 或 null(无需更新)
*/
private function calculateCourseStatus($timeSlot, $currentTime)
{
if (empty($timeSlot)) {
return null;
}
// 解析时间段,格式如 "11:30-12:30"
$timeParts = explode('-', $timeSlot);
if (count($timeParts) != 2) {
Log::write("时间段格式错误: {$timeSlot}");
return null;
}
$startTime = trim($timeParts[0]) . ':00'; // 转换为 "11:30:00"
$endTime = trim($timeParts[1]) . ':00'; // 转换为 "12:30:00"
$currentTimestamp = strtotime($currentTime);
$startTimestamp = strtotime($startTime);
$endTimestamp = strtotime($endTime);
$sixHoursBeforeStart = $startTimestamp - (6 * 3600); // 开始时间前6小时
if ($currentTimestamp <= $sixHoursBeforeStart) {
// 当前时间距离开始时间超过6小时,不需要更新状态
return null;
} elseif ($currentTimestamp <= $startTimestamp) {
// 当前时间在开始前6小时内,状态为即将开始
return 'upcoming';
} elseif ($currentTimestamp >= $startTimestamp && $currentTimestamp <= $endTimestamp) {
// 当前时间在课程时间范围内,状态为进行中
return 'ongoing';
} elseif ($currentTimestamp > $endTimestamp) {
// 当前时间已过结束时间,状态为已结束
return 'completed';
}
return null;
}
/**
* 计算下周同一天的日期
* @param string $courseDate 课程日期,格式如 "2025-01-08"
* @return string 下周同一天的日期
*/
private function getNextWeekSameDay($courseDate)
{
$timestamp = strtotime($courseDate);
$nextWeekTimestamp = $timestamp + (7 * 24 * 3600); // 加7天
return date('Y-m-d', $nextWeekTimestamp);
}
/**
* 发送课程即将过期的警告通知
* @param Course $course 课程对象
*/
protected function sendExpiryWarning($course)
{
// 这里可以实现具体的通知逻辑
// 比如发送邮件、短信、系统内通知等
Log::write("发送过期提醒通知:课程 {$course->course_name} 将在 {$course->valid_until} 过期");
// 示例:可以调用通知服务
// NotificationService::send([
// 'type' => 'course_expiry_warning',
// 'course_id' => $course->id,
// 'message' => "您的课程《{$course->course_name}》将在{$course->valid_until}过期"
// ]);
}
}

277
niucloud/app/job/transfer/schedule/StaffAttendanceJob.php

@ -0,0 +1,277 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\job\transfer\schedule;
use app\model\staff\Staff;
use app\model\staff\StaffAttendance;
use core\base\BaseJob;
use think\facade\Log;
/**
* 定时处理员工考勤状态任务
* 自动生成考勤记录、处理迟到早退、计算工作时长等
*/
class StaffAttendanceJob extends BaseJob
{
/**
* 执行定时任务
* @param mixed ...$data 任务参数
* @return bool 处理结果
*/
public function doJob(...$data)
{
Log::write('开始执行员工考勤状态处理任务');
try {
$currentDate = date('Y-m-d');
$currentTime = date('H:i:s');
// 处理当日考勤记录生成
$generateResult = $this->generateDailyAttendance($currentDate);
// 处理迟到状态更新
$lateResult = $this->processLateArrivals($currentDate, $currentTime);
// 处理早退状态更新
$earlyResult = $this->processEarlyDepartures($currentDate, $currentTime);
// 处理缺勤状态更新
$absentResult = $this->processAbsences($currentDate);
// 计算工作时长
$workHoursResult = $this->calculateWorkHours($currentDate);
Log::write('员工考勤状态处理任务执行完成', [
'generated_records' => $generateResult['count'],
'late_arrivals' => $lateResult['count'],
'early_departures' => $earlyResult['count'],
'absences' => $absentResult['count'],
'calculated_hours' => $workHoursResult['count']
]);
return true;
} catch (\Exception $e) {
Log::write('员工考勤状态处理任务执行失败:' . $e->getMessage());
throw $e;
}
}
/**
* 生成当日考勤记录
* @param string $date 日期
* @return array 处理结果
*/
protected function generateDailyAttendance($date)
{
// 获取所有在职员工
$activeStaff = Staff::where('status', 1)->select();
$count = 0;
foreach ($activeStaff as $staff) {
// 检查是否已有当日考勤记录
$exists = StaffAttendance::where('staff_id', $staff->id)
->where('attendance_date', $date)
->count();
if ($exists == 0) {
// 创建考勤记录
$attendance = new StaffAttendance();
$attendance->staff_id = $staff->id;
$attendance->attendance_date = $date;
$attendance->status = 'pending'; // 待考勤状态
$attendance->work_schedule_start = '09:00:00'; // 默认上班时间
$attendance->work_schedule_end = '18:00:00'; // 默认下班时间
$attendance->created_at = date('Y-m-d H:i:s');
$attendance->save();
$count++;
Log::write("为员工生成考勤记录:员工ID {$staff->id}, 日期: {$date}");
}
}
return ['count' => $count];
}
/**
* 处理迟到状态
* @param string $date 日期
* @param string $currentTime 当前时间
* @return array 处理结果
*/
protected function processLateArrivals($date, $currentTime)
{
// 查找今日还未签到但已过上班时间的员工
$lateThreshold = '09:30:00'; // 迟到阈值:9:30后算迟到
$lateAttendances = StaffAttendance::where('attendance_date', $date)
->where('check_in_time', null)
->where('work_schedule_start', '<', $currentTime)
->select();
$count = 0;
foreach ($lateAttendances as $attendance) {
if ($currentTime > $lateThreshold) {
$attendance->status = 'late';
$attendance->late_minutes = $this->calculateLateMinutes($attendance->work_schedule_start, $currentTime);
$attendance->save();
$count++;
Log::write("员工迟到:员工ID {$attendance->staff_id}, 迟到 {$attendance->late_minutes} 分钟");
}
}
return ['count' => $count];
}
/**
* 处理早退状态
* @param string $date 日期
* @param string $currentTime 当前时间
* @return array 处理结果
*/
protected function processEarlyDepartures($date, $currentTime)
{
// 查找今日已签到但提前签退的员工
$earlyAttendances = StaffAttendance::where('attendance_date', $date)
->where('check_in_time', '<>', null)
->where('check_out_time', '<>', null)
->where('check_out_time', '<', 'work_schedule_end')
->select();
$count = 0;
foreach ($earlyAttendances as $attendance) {
$attendance->status = 'early_departure';
$attendance->early_minutes = $this->calculateEarlyMinutes($attendance->check_out_time, $attendance->work_schedule_end);
$attendance->save();
$count++;
Log::write("员工早退:员工ID {$attendance->staff_id}, 早退 {$attendance->early_minutes} 分钟");
}
return ['count' => $count];
}
/**
* 处理缺勤状态
* @param string $date 日期
* @return array 处理结果
*/
protected function processAbsences($date)
{
$currentTime = date('H:i:s');
$absentThreshold = '18:30:00'; // 18:30后还未签到算缺勤
if ($currentTime < $absentThreshold) {
return ['count' => 0]; // 还没到判断缺勤的时间
}
// 查找今日全天未签到的员工
$absentAttendances = StaffAttendance::where('attendance_date', $date)
->where('check_in_time', null)
->where('status', '!=', 'leave') // 排除请假的员工
->select();
$count = 0;
foreach ($absentAttendances as $attendance) {
$attendance->status = 'absent';
$attendance->save();
$count++;
Log::write("员工缺勤:员工ID {$attendance->staff_id}, 日期: {$date}");
}
return ['count' => $count];
}
/**
* 计算工作时长
* @param string $date 日期
* @return array 处理结果
*/
protected function calculateWorkHours($date)
{
// 查找当日已完成签到签退的考勤记录
$completedAttendances = StaffAttendance::where('attendance_date', $date)
->where('check_in_time', '<>', null)
->where('check_out_time', '<>', null)
->where('work_hours', null) // 还未计算工作时长的
->select();
$count = 0;
foreach ($completedAttendances as $attendance) {
$workHours = $this->calculateHoursBetween($attendance->check_in_time, $attendance->check_out_time);
$attendance->work_hours = $workHours;
$attendance->save();
$count++;
Log::write("计算员工工作时长:员工ID {$attendance->staff_id}, 工作时长: {$workHours} 小时");
}
return ['count' => $count];
}
/**
* 计算迟到分钟数
* @param string $scheduleTime 计划时间
* @param string $actualTime 实际时间
* @return int 迟到分钟数
*/
protected function calculateLateMinutes($scheduleTime, $actualTime)
{
$schedule = strtotime($scheduleTime);
$actual = strtotime($actualTime);
if ($actual > $schedule) {
return intval(($actual - $schedule) / 60);
}
return 0;
}
/**
* 计算早退分钟数
* @param string $actualTime 实际时间
* @param string $scheduleTime 计划时间
* @return int 早退分钟数
*/
protected function calculateEarlyMinutes($actualTime, $scheduleTime)
{
$actual = strtotime($actualTime);
$schedule = strtotime($scheduleTime);
if ($schedule > $actual) {
return intval(($schedule - $actual) / 60);
}
return 0;
}
/**
* 计算两个时间之间的小时数
* @param string $startTime 开始时间
* @param string $endTime 结束时间
* @return float 小时数
*/
protected function calculateHoursBetween($startTime, $endTime)
{
$start = strtotime($startTime);
$end = strtotime($endTime);
if ($end > $start) {
return round(($end - $start) / 3600, 2);
}
return 0;
}
}

372
niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php

@ -0,0 +1,372 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\job\transfer\schedule;
use app\model\member\Member;
use app\model\course\Course;
use app\model\course_booking\CourseBooking;
use app\model\notification\Notification;
use core\base\BaseJob;
use think\facade\Log;
/**
* 定时检查学员课程有效期和课时数量发送提醒任务
* 监控学员课程状态,及时发送相关提醒通知
*/
class StudentCourseReminderJob extends BaseJob
{
/**
* 执行定时任务
* @param mixed ...$data 任务参数
* @return bool 处理结果
*/
public function doJob(...$data)
{
Log::write('开始执行学员课程提醒任务');
try {
// 检查课程有效期提醒
$expiryResult = $this->checkCourseExpiryReminders();
// 检查课时余量提醒
$hoursResult = $this->checkRemainingHoursReminders();
// 检查预约课程提醒
$bookingResult = $this->checkUpcomingBookingReminders();
// 检查长期未上课提醒
$inactiveResult = $this->checkInactiveStudentReminders();
// 检查课程完成祝贺
$completionResult = $this->checkCourseCompletionCongratulations();
Log::write('学员课程提醒任务执行完成', [
'expiry_reminders' => $expiryResult['count'],
'hours_reminders' => $hoursResult['count'],
'booking_reminders' => $bookingResult['count'],
'inactive_reminders' => $inactiveResult['count'],
'completion_congratulations' => $completionResult['count']
]);
return true;
} catch (\Exception $e) {
Log::write('学员课程提醒任务执行失败:' . $e->getMessage());
throw $e;
}
}
/**
* 检查课程有效期提醒
* @return array 处理结果
*/
protected function checkCourseExpiryReminders()
{
$currentDate = date('Y-m-d');
// 查找7天内即将过期的课程
$expiringCourses = Course::where('valid_until', '>', $currentDate)
->where('valid_until', '<=', date('Y-m-d', strtotime('+7 days')))
->where('status', 1)
->select();
$count = 0;
foreach ($expiringCourses as $course) {
// 检查是否已发送过提醒
$reminderSent = $this->checkReminderSent($course->member_id, 'course_expiry', $course->id);
if (!$reminderSent) {
$daysLeft = ceil((strtotime($course->valid_until) - strtotime($currentDate)) / 86400);
$this->sendCourseExpiryReminder($course, $daysLeft);
$this->recordReminderSent($course->member_id, 'course_expiry', $course->id);
$count++;
Log::write("发送课程过期提醒:学员ID {$course->member_id}, 课程ID {$course->id}, 剩余 {$daysLeft} 天");
}
}
return ['count' => $count];
}
/**
* 检查课时余量提醒
* @return array 处理结果
*/
protected function checkRemainingHoursReminders()
{
// 查找剩余课时少于3节的有效课程
$lowHoursCourses = Course::where('remaining_hours', '>', 0)
->where('remaining_hours', '<=', 3)
->where('status', 1)
->select();
$count = 0;
foreach ($lowHoursCourses as $course) {
// 检查是否已发送过提醒
$reminderSent = $this->checkReminderSent($course->member_id, 'low_hours', $course->id);
if (!$reminderSent) {
$this->sendLowHoursReminder($course);
$this->recordReminderSent($course->member_id, 'low_hours', $course->id);
$count++;
Log::write("发送课时不足提醒:学员ID {$course->member_id}, 课程ID {$course->id}, 剩余 {$course->remaining_hours} 课时");
}
}
return ['count' => $count];
}
/**
* 检查预约课程提醒
* @return array 处理结果
*/
protected function checkUpcomingBookingReminders()
{
$tomorrow = date('Y-m-d', strtotime('+1 day'));
// 查找明天的预约课程
$tomorrowBookings = CourseBooking::where('booking_date', $tomorrow)
->where('status', 1)
->select();
$count = 0;
foreach ($tomorrowBookings as $booking) {
// 检查是否已发送过提醒
$reminderSent = $this->checkReminderSent($booking->member_id, 'booking_reminder', $booking->id);
if (!$reminderSent) {
$this->sendBookingReminder($booking);
$this->recordReminderSent($booking->member_id, 'booking_reminder', $booking->id);
$count++;
Log::write("发送预约提醒:学员ID {$booking->member_id}, 预约ID {$booking->id}");
}
}
return ['count' => $count];
}
/**
* 检查长期未上课提醒
* @return array 处理结果
*/
protected function checkInactiveStudentReminders()
{
$thirtyDaysAgo = date('Y-m-d', strtotime('-30 days'));
// 查找有有效课程但30天内没有上课的学员
$inactiveMembers = Member::whereHas('courses', function($query) {
$query->where('status', 1)->where('remaining_hours', '>', 0);
})->whereDoesntHave('courseBookings', function($query) use ($thirtyDaysAgo) {
$query->where('booking_date', '>=', $thirtyDaysAgo)
->where('status', 'completed');
})->select();
$count = 0;
foreach ($inactiveMembers as $member) {
// 检查是否已发送过提醒
$reminderSent = $this->checkReminderSent($member->id, 'inactive_student', $member->id);
if (!$reminderSent) {
$this->sendInactiveStudentReminder($member);
$this->recordReminderSent($member->id, 'inactive_student', $member->id);
$count++;
Log::write("发送长期未上课提醒:学员ID {$member->id}");
}
}
return ['count' => $count];
}
/**
* 检查课程完成祝贺
* @return array 处理结果
*/
protected function checkCourseCompletionCongratulations()
{
// 查找今日刚完成的课程(剩余课时为0且今日有完成的预约)
$today = date('Y-m-d');
$completedCourses = Course::where('remaining_hours', 0)
->where('status', 1)
->whereHas('courseBookings', function($query) use ($today) {
$query->where('booking_date', $today)
->where('status', 'completed');
})->select();
$count = 0;
foreach ($completedCourses as $course) {
// 检查是否已发送过祝贺
$congratsSent = $this->checkReminderSent($course->member_id, 'course_completion', $course->id);
if (!$congratsSent) {
$this->sendCourseCompletionCongratulations($course);
$this->recordReminderSent($course->member_id, 'course_completion', $course->id);
// 更新课程状态为已完成
$course->status = 3; // 假设3为已完成状态
$course->completed_at = date('Y-m-d H:i:s');
$course->save();
$count++;
Log::write("发送课程完成祝贺:学员ID {$course->member_id}, 课程ID {$course->id}");
}
}
return ['count' => $count];
}
/**
* 发送课程过期提醒
* @param Course $course 课程对象
* @param int $daysLeft 剩余天数
*/
protected function sendCourseExpiryReminder($course, $daysLeft)
{
$member = Member::find($course->member_id);
if (!$member) return;
$message = "亲爱的{$member->nickname},您的课程《{$course->course_name}》将在{$daysLeft}天后过期,请尽快安排上课时间。";
$this->createNotification($member->id, 'course_expiry', '课程即将过期提醒', $message, [
'course_id' => $course->id,
'days_left' => $daysLeft
]);
}
/**
* 发送课时不足提醒
* @param Course $course 课程对象
*/
protected function sendLowHoursReminder($course)
{
$member = Member::find($course->member_id);
if (!$member) return;
$message = "亲爱的{$member->nickname},您的课程《{$course->course_name}》还剩{$course->remaining_hours}节课,建议及时续费以确保学习连续性。";
$this->createNotification($member->id, 'low_hours', '课时不足提醒', $message, [
'course_id' => $course->id,
'remaining_hours' => $course->remaining_hours
]);
}
/**
* 发送预约课程提醒
* @param CourseBooking $booking 预约对象
*/
protected function sendBookingReminder($booking)
{
$member = Member::find($booking->member_id);
$course = Course::find($booking->course_id);
if (!$member || !$course) return;
$message = "亲爱的{$member->nickname},您预约的课程《{$course->course_name}》将在明天{$booking->time_slot}开始,请准时参加。";
$this->createNotification($member->id, 'booking_reminder', '课程预约提醒', $message, [
'booking_id' => $booking->id,
'course_id' => $booking->course_id,
'booking_date' => $booking->booking_date,
'time_slot' => $booking->time_slot
]);
}
/**
* 发送长期未上课提醒
* @param Member $member 学员对象
*/
protected function sendInactiveStudentReminder($member)
{
$message = "亲爱的{$member->nickname},您已经很久没有上课了。坚持学习才能看到更好的效果,快来预约课程吧!";
$this->createNotification($member->id, 'inactive_student', '久未上课提醒', $message, [
'member_id' => $member->id
]);
}
/**
* 发送课程完成祝贺
* @param Course $course 课程对象
*/
protected function sendCourseCompletionCongratulations($course)
{
$member = Member::find($course->member_id);
if (!$member) return;
$message = "恭喜{$member->nickname}!您已成功完成《{$course->course_name}》课程的全部学习,感谢您的坚持和努力!";
$this->createNotification($member->id, 'course_completion', '课程完成祝贺', $message, [
'course_id' => $course->id,
'completion_date' => date('Y-m-d')
]);
}
/**
* 检查是否已发送过提醒
* @param int $memberId 学员ID
* @param string $type 提醒类型
* @param int $relatedId 相关ID
* @return bool
*/
protected function checkReminderSent($memberId, $type, $relatedId)
{
$today = date('Y-m-d');
$exists = Notification::where('member_id', $memberId)
->where('type', $type)
->where('related_id', $relatedId)
->where('created_at', '>=', $today . ' 00:00:00')
->count();
return $exists > 0;
}
/**
* 记录已发送的提醒
* @param int $memberId 学员ID
* @param string $type 提醒类型
* @param int $relatedId 相关ID
*/
protected function recordReminderSent($memberId, $type, $relatedId)
{
// 这个方法可以用于记录提醒发送日志,避免重复发送
Log::write("记录提醒发送:学员ID {$memberId}, 类型: {$type}, 相关ID: {$relatedId}");
}
/**
* 创建通知记录
* @param int $memberId 学员ID
* @param string $type 通知类型
* @param string $title 标题
* @param string $message 消息内容
* @param array $extra 额外数据
*/
protected function createNotification($memberId, $type, $title, $message, $extra = [])
{
$notification = new Notification();
$notification->member_id = $memberId;
$notification->type = $type;
$notification->title = $title;
$notification->message = $message;
$notification->extra_data = json_encode($extra);
$notification->status = 'sent';
$notification->created_at = date('Y-m-d H:i:s');
$notification->save();
// 这里可以扩展实际的通知发送逻辑
// 比如调用短信服务、邮件服务、推送服务等
Log::write("创建通知:学员ID {$memberId}, 类型: {$type}, 标题: {$title}");
}
}

310
niucloud/app/job/transfer/schedule/TeachingServiceJob.php

@ -0,0 +1,310 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\job\transfer\schedule;
use app\model\staff\Staff;
use app\model\service\TeachingService;
use app\model\course\Course;
use app\model\course_schedule\CourseSchedule;
use core\base\BaseJob;
use think\facade\Log;
/**
* 定时为教务或教练创建服务内容任务
* 根据教务/教练的课程安排自动生成服务内容记录
*/
class TeachingServiceJob extends BaseJob
{
/**
* 执行定时任务
* @param mixed ...$data 任务参数
* @return bool 处理结果
*/
public function doJob(...$data)
{
Log::write('开始执行教务/教练服务内容生成任务');
try {
$currentDate = date('Y-m-d');
// 为教务人员创建服务内容
$academicResult = $this->createAcademicServices($currentDate);
// 为教练创建服务内容
$coachResult = $this->createCoachServices($currentDate);
// 为助教创建服务内容
$assistantResult = $this->createAssistantServices($currentDate);
// 更新服务完成状态
$completionResult = $this->updateServiceCompletion($currentDate);
Log::write('教务/教练服务内容生成任务执行完成:' . print_r([
'academic_services' => $academicResult['count'],
'coach_services' => $coachResult['count'],
'assistant_services' => $assistantResult['count'],
'completed_services' => $completionResult['count']
], true));
return true;
} catch (\Exception $e) {
Log::write('教务/教练服务内容生成任务执行失败:' . $e->getMessage());
throw $e;
}
}
/**
* 为教务人员创建服务内容
* @param string $date 日期
* @return array 处理结果
*/
protected function createAcademicServices($date)
{
// 获取所有教务人员
$academicStaff = Staff::where('role_type', 'academic')
->where('status', 1)
->select();
$count = 0;
foreach ($academicStaff as $staff) {
// 检查今日是否已有服务记录
$existingService = TeachingService::where('staff_id', $staff->id)
->where('service_date', $date)
->count();
if ($existingService == 0) {
// 创建教务服务内容
$service = new TeachingService();
$service->staff_id = $staff->id;
$service->staff_name = $staff->real_name;
$service->service_date = $date;
$service->service_type = 'academic';
$service->service_content = $this->generateAcademicServiceContent($staff, $date);
$service->status = 'pending';
$service->created_at = date('Y-m-d H:i:s');
$service->save();
$count++;
Log::write("为教务人员创建服务内容:员工ID {$staff->id}, 姓名: {$staff->real_name}");
}
}
return ['count' => $count];
}
/**
* 为教练创建服务内容
* @param string $date 日期
* @return array 处理结果
*/
protected function createCoachServices($date)
{
// 获取所有教练
$coaches = Staff::where('role_type', 'coach')
->where('status', 1)
->select();
$count = 0;
foreach ($coaches as $coach) {
// 获取教练当日的课程安排
$todaySchedules = CourseSchedule::where('coach_id', $coach->id)
->where('course_date', $date)
->select();
if (count($todaySchedules) > 0) {
// 检查是否已有服务记录
$existingService = TeachingService::where('staff_id', $coach->id)
->where('service_date', $date)
->count();
if ($existingService == 0) {
// 创建教练服务内容
$service = new TeachingService();
$service->staff_id = $coach->id;
$service->staff_name = $coach->real_name;
$service->service_date = $date;
$service->service_type = 'coach';
$service->service_content = $this->generateCoachServiceContent($coach, $todaySchedules);
$service->scheduled_courses = count($todaySchedules);
$service->status = 'scheduled';
$service->created_at = date('Y-m-d H:i:s');
$service->save();
$count++;
Log::write("为教练创建服务内容:员工ID {$coach->id}, 课程数: " . count($todaySchedules));
}
}
}
return ['count' => $count];
}
/**
* 为助教创建服务内容
* @param string $date 日期
* @return array 处理结果
*/
protected function createAssistantServices($date)
{
// 获取所有助教
$assistants = Staff::where('role_type', 'assistant')
->where('status', 1)
->select();
$count = 0;
foreach ($assistants as $assistant) {
// 获取助教需要协助的课程
$assistantSchedules = CourseSchedule::where('assistant_id', $assistant->id)
->where('course_date', $date)
->select();
if (count($assistantSchedules) > 0) {
// 检查是否已有服务记录
$existingService = TeachingService::where('staff_id', $assistant->id)
->where('service_date', $date)
->count();
if ($existingService == 0) {
// 创建助教服务内容
$service = new TeachingService();
$service->staff_id = $assistant->id;
$service->staff_name = $assistant->real_name;
$service->service_date = $date;
$service->service_type = 'assistant';
$service->service_content = $this->generateAssistantServiceContent($assistant, $assistantSchedules);
$service->scheduled_courses = count($assistantSchedules);
$service->status = 'scheduled';
$service->created_at = date('Y-m-d H:i:s');
$service->save();
$count++;
Log::write("为助教创建服务内容:员工ID {$assistant->id}, 协助课程数: " . count($assistantSchedules));
}
}
}
return ['count' => $count];
}
/**
* 更新服务完成状态
* @param string $date 日期
* @return array 处理结果
*/
protected function updateServiceCompletion($date)
{
$currentTime = date('H:i:s');
$completionThreshold = '22:00:00'; // 22:00后自动标记为完成
if ($currentTime < $completionThreshold) {
return ['count' => 0];
}
// 更新当日未完成的服务为已完成状态
$pendingServices = TeachingService::where('service_date', $date)
->where('status', 'scheduled')
->select();
$count = 0;
foreach ($pendingServices as $service) {
$service->status = 'completed';
$service->completed_at = date('Y-m-d H:i:s');
$service->save();
$count++;
Log::write("自动标记服务完成:员工ID {$service->staff_id}, 服务类型: {$service->service_type}");
}
return ['count' => $count];
}
/**
* 生成教务服务内容
* @param Staff $staff 员工对象
* @param string $date 日期
* @return string 服务内容描述
*/
protected function generateAcademicServiceContent($staff, $date)
{
$content = [];
$content[] = "日期:{$date}";
$content[] = "教务人员:{$staff->real_name}";
$content[] = "服务内容:";
$content[] = "1. 学员课程安排和调度管理";
$content[] = "2. 教练课表协调和优化";
$content[] = "3. 学员咨询和问题解答";
$content[] = "4. 课程质量跟踪和反馈收集";
$content[] = "5. 学员学习进度监控";
$content[] = "6. 教学资源配置和管理";
return implode("\n", $content);
}
/**
* 生成教练服务内容
* @param Staff $coach 教练对象
* @param array $schedules 课程安排
* @return string 服务内容描述
*/
protected function generateCoachServiceContent($coach, $schedules)
{
$content = [];
$content[] = "教练:{$coach->real_name}";
$content[] = "预定课程数:" . count($schedules);
$content[] = "课程安排:";
foreach ($schedules as $schedule) {
$course = Course::find($schedule->course_id);
$courseName = $course ? $course->course_name : "未知课程";
$content[] = "- {$schedule->time_slot}: {$courseName} (场地ID: {$schedule->venue_id})";
}
$content[] = "服务职责:";
$content[] = "1. 按时到达指定场地进行授课";
$content[] = "2. 确保课程质量和安全性";
$content[] = "3. 记录学员学习情况和进度";
$content[] = "4. 提供专业指导和建议";
$content[] = "5. 维护教学设备和场地整洁";
return implode("\n", $content);
}
/**
* 生成助教服务内容
* @param Staff $assistant 助教对象
* @param array $schedules 课程安排
* @return string 服务内容描述
*/
protected function generateAssistantServiceContent($assistant, $schedules)
{
$content = [];
$content[] = "助教:{$assistant->real_name}";
$content[] = "协助课程数:" . count($schedules);
$content[] = "协助安排:";
foreach ($schedules as $schedule) {
$course = Course::find($schedule->course_id);
$courseName = $course ? $course->course_name : "未知课程";
$content[] = "- {$schedule->time_slot}: 协助 {$courseName}";
}
$content[] = "服务职责:";
$content[] = "1. 协助主教练进行课程教学";
$content[] = "2. 维护课堂秩序和安全";
$content[] = "3. 帮助学员纠正动作和姿势";
$content[] = "4. 准备和整理教学器材";
$content[] = "5. 记录学员出勤和表现";
return implode("\n", $content);
}
}

14
niucloud/app/model/sys/SysMenu.php

@ -23,7 +23,7 @@ use think\model\concern\SoftDelete;
*/ */
class SysMenu extends BaseModel class SysMenu extends BaseModel
{ {
use SoftDelete; // use SoftDelete; // 暂时禁用软删除
/** /**
* 数据表主键 * 数据表主键
@ -35,7 +35,13 @@ class SysMenu extends BaseModel
* 模型名称 * 模型名称
* @var string * @var string
*/ */
protected $name = 'sys_menu'; protected $name = 'sys_menus';
/**
* 表名(不使用前缀)
* @var string
*/
protected $table = 'sys_menus';
/** /**
* 追加字段 * 追加字段
* @var array * @var array
@ -46,12 +52,12 @@ class SysMenu extends BaseModel
* 定义软删除标记字段 * 定义软删除标记字段
* @var string * @var string
*/ */
protected $deleteTime = 'delete_time'; // protected $deleteTime = 'delete_time';
/** /**
* 定义软删除字段的默认值 * 定义软删除字段的默认值
* @var int * @var int
*/ */
protected $defaultSoftDelete = 0; // protected $defaultSoftDelete = 0;
/** /**
* 菜单类型 * 菜单类型

26
niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php

@ -100,8 +100,8 @@ class CampusPersonRoleService extends BaseAdminService
if($this->model->where(['person_id' => $data['person_id']])->find()){ if($this->model->where(['person_id' => $data['person_id']])->find()){
return fail("重复操作"); return fail("重复操作");
} }
$role = new SysRole(); $db = app()->db;
$data['dept_id'] = $role->where(['role_id' => $data['role_id']])->value("dept_id"); $data['dept_id'] = $db->table('school_sys_role')->where('role_id', $data['role_id'])->value('dept_id');
$res = $this->model->create($data); $res = $this->model->create($data);
return success("操作成功"); return success("操作成功");
@ -125,8 +125,8 @@ class CampusPersonRoleService extends BaseAdminService
$personnel_summary->where(['id' => $tasks['id']])->update(['task_num' => $tasks['task_num']]); $personnel_summary->where(['id' => $tasks['id']])->update(['task_num' => $tasks['task_num']]);
} }
$role = new SysRole(); $db = app()->db;
$data['dept_id'] = $role->where(['role_id' => $data['role_id']])->value("dept_id"); $data['dept_id'] = $db->table('school_sys_role')->where('role_id', $data['role_id'])->value('dept_id');
$this->model->where([['id', '=', $id]])->update($data); $this->model->where([['id', '=', $id]])->update($data);
return success("操作成功"); return success("操作成功");
@ -159,8 +159,22 @@ class CampusPersonRoleService extends BaseAdminService
} }
public function getSysRoleAll($data){ public function getSysRoleAll($data){
$sysRoleModel = new SysRole(); // 直接查询school_sys_role表,过滤掉超级管理员(role_key为空或role_name为超级管理员)
return $sysRoleModel->where(['dept_id' => $data['dept_id']])->select()->toArray(); $db = app()->db;
$query = $db->table('school_sys_role')
->where('status', 1)
->where(function ($query) {
$query->whereNotNull('role_key')
->where('role_key', '<>', '')
->where('role_name', '<>', '超级管理员');
});
// 如果dept_id不为0,则按部门过滤;如果为0则显示所有可用角色
if (isset($data['dept_id']) && $data['dept_id'] > 0) {
$query->where('dept_id', $data['dept_id']);
}
return $query->select()->toArray();
} }
public function getDepartmentsAll(){ public function getDepartmentsAll(){

231
niucloud/app/service/admin/sys/SysMenuService.php

@ -0,0 +1,231 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\admin\sys;
use app\model\sys\SysMenu;
use core\base\BaseAdminService;
use core\exception\AdminException;
use think\facade\Db;
/**
* 系统菜单服务类
*/
class SysMenuService extends BaseAdminService
{
public function __construct()
{
parent::__construct();
$this->model = new SysMenu();
}
/**
* 获取菜单分页列表
* @param array $where
* @return array
*/
public function getPage(array $where = [])
{
$field = 'id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description, created_at, updated_at';
$order = 'sort_order ASC, id DESC';
$search_model = $this->model
->withSearch(['keyword', 'status'], $where)
->field($field)
->order($order);
$list = $this->pageQuery($search_model, $where);
return $list;
}
/**
* 获取菜单信息
* @param int $id
* @return array
*/
public function getInfo(int $id)
{
$field = 'id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description, created_at, updated_at';
$info = $this->model->field($field)->where([['id', '=', $id]])->findOrEmpty()->toArray();
if (empty($info)) {
throw new AdminException('菜单不存在');
}
// 解析JSON参数
if (!empty($info['menu_params'])) {
$info['menu_params'] = json_decode($info['menu_params'], true) ?? [];
} else {
$info['menu_params'] = [];
}
return $info;
}
/**
* 添加菜单
* @param array $data
* @return mixed
*/
public function add(array $data)
{
// 处理JSON参数
if (isset($data['menu_params']) && is_array($data['menu_params'])) {
$data['menu_params'] = json_encode($data['menu_params'], JSON_UNESCAPED_UNICODE);
}
$res = $this->model->create($data);
return $res->id;
}
/**
* 编辑菜单
* @param int $id
* @param array $data
* @return bool
*/
public function edit(int $id, array $data)
{
$info = $this->model->findOrEmpty($id);
if ($info->isEmpty()) {
throw new AdminException('菜单不存在');
}
// 处理JSON参数
if (isset($data['menu_params']) && is_array($data['menu_params'])) {
$data['menu_params'] = json_encode($data['menu_params'], JSON_UNESCAPED_UNICODE);
}
$this->model->where([['id', '=', $id]])->update($data);
return true;
}
/**
* 删除菜单
* @param int $id
* @return bool
*/
public function del(int $id)
{
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new AdminException('菜单不存在');
}
// 检查是否有角色在使用此菜单
$result = Db::query("SELECT COUNT(*) as count FROM role_menu_permissions WHERE menu_id = ?", [$id]);
$roleMenuCount = $result[0]['count'] ?? 0;
if ($roleMenuCount > 0) {
throw new AdminException('该菜单已被角色使用,无法删除');
}
$res = $model->delete();
return $res !== false;
}
/**
* 修改菜单状态
* @param int $id
* @param int $status
* @return bool
*/
public function modifyStatus(int $id, int $status)
{
$this->model->where([['id', '=', $id]])->update(['status' => $status]);
return true;
}
/**
* 获取所有菜单(用于角色权限配置)
* @return array
*/
public function getAllMenus()
{
$list = $this->model
->field('id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description')
->order('sort_order ASC, id ASC')
->select()
->toArray();
// 处理参数
foreach ($list as &$item) {
if (!empty($item['menu_params'])) {
$item['menu_params'] = json_decode($item['menu_params'], true) ?? [];
} else {
$item['menu_params'] = [];
}
}
return $list;
}
/**
* 获取角色菜单权限
* @param int $roleId
* @return array
*/
public function getRoleMenus(int $roleId)
{
// 使用原生SQL查询避免表前缀问题
$sql = "SELECT m.id, m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order
FROM role_menu_permissions rmp
LEFT JOIN sys_menus m ON rmp.menu_id = m.id
WHERE rmp.role_id = ? AND rmp.is_enabled = 1
ORDER BY m.sort_order ASC";
$menuList = Db::query($sql, [$roleId]);
// 处理参数
foreach ($menuList as &$item) {
if (!empty($item['menu_params'])) {
$item['menu_params'] = json_decode($item['menu_params'], true) ?? [];
} else {
$item['menu_params'] = [];
}
}
return $menuList;
}
/**
* 设置角色菜单权限
* @param int $roleId
* @param array $menuIds
* @return bool
*/
public function setRoleMenus(int $roleId, array $menuIds)
{
Db::startTrans();
try {
// 删除原有权限
Db::execute("DELETE FROM role_menu_permissions WHERE role_id = ?", [$roleId]);
// 添加新权限
if (!empty($menuIds)) {
$values = [];
$params = [];
foreach ($menuIds as $menuId) {
$values[] = "(?, ?, 1, NOW(), NOW())";
$params[] = $roleId;
$params[] = $menuId;
}
$sql = "INSERT INTO role_menu_permissions (role_id, menu_id, is_enabled, created_at, updated_at) VALUES " . implode(', ', $values);
Db::execute($sql, $params);
}
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw new AdminException('设置菜单权限失败:' . $e->getMessage());
}
}
}

106
niucloud/app/service/admin/uniapp/UniappAuthService.php

@ -0,0 +1,106 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\admin\uniapp;
use core\base\BaseAdminService;
use core\exception\AdminException;
use think\facade\Db;
/**
* UniApp权限管理服务类
*/
class UniappAuthService extends BaseAdminService
{
/**
* 获取所有UniApp菜单(用于权限配置)
* @return array
*/
public function getAllMenus()
{
$sql = "SELECT id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description
FROM sys_menus
WHERE status = 1
ORDER BY sort_order ASC, id ASC";
$list = Db::query($sql);
// 处理参数
foreach ($list as &$item) {
if (!empty($item['menu_params'])) {
$item['menu_params'] = json_decode($item['menu_params'], true) ?? [];
} else {
$item['menu_params'] = [];
}
}
return $list;
}
/**
* 获取角色的UniApp菜单权限
* @param int $roleId
* @return array
*/
public function getRoleMenus(int $roleId)
{
$sql = "SELECT m.id, m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order
FROM role_menu_permissions rmp
LEFT JOIN sys_menus m ON rmp.menu_id = m.id
WHERE rmp.role_id = ? AND rmp.is_enabled = 1
ORDER BY m.sort_order ASC";
$menuList = Db::query($sql, [$roleId]);
// 处理参数
foreach ($menuList as &$item) {
if (!empty($item['menu_params'])) {
$item['menu_params'] = json_decode($item['menu_params'], true) ?? [];
} else {
$item['menu_params'] = [];
}
}
return $menuList;
}
/**
* 设置角色的UniApp菜单权限
* @param int $roleId
* @param array $menuIds
* @return bool
*/
public function setRoleMenus(int $roleId, array $menuIds)
{
Db::startTrans();
try {
// 删除原有权限
Db::execute("DELETE FROM role_menu_permissions WHERE role_id = ?", [$roleId]);
// 添加新权限
if (!empty($menuIds)) {
$values = [];
$params = [];
foreach ($menuIds as $menuId) {
$values[] = "(?, ?, 1, NOW(), NOW())";
$params[] = $roleId;
$params[] = $menuId;
}
$sql = "INSERT INTO role_menu_permissions (role_id, menu_id, is_enabled, created_at, updated_at) VALUES " . implode(', ', $values);
Db::execute($sql, $params);
}
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw new AdminException('设置UniApp菜单权限失败:' . $e->getMessage());
}
}
}

31
niucloud/app/service/api/apiService/ResourceSharingService.php

@ -357,6 +357,18 @@ class ResourceSharingService extends BaseApiService
// 查询开单状态 // 查询开单状态
$order_status = []; $order_status = [];
if (!empty($resource_ids)) { if (!empty($resource_ids)) {
// 首先查询是否有付费订单
$order_model = new OrderTable();
$paid_orders = $order_model
->whereIn('resource_id', $resource_ids)
->where('order_status', 'paid')
->field('resource_id')
->select()
->toArray();
$paid_resource_ids = array_column($paid_orders, 'resource_id');
// 查询六速表的开单状态
$six_speed_model = new \app\model\six_speed\SixSpeed(); $six_speed_model = new \app\model\six_speed\SixSpeed();
$six_speed_records = $six_speed_model $six_speed_records = $six_speed_model
->whereIn('resource_id', $resource_ids) ->whereIn('resource_id', $resource_ids)
@ -364,8 +376,23 @@ class ResourceSharingService extends BaseApiService
->select() ->select()
->toArray(); ->toArray();
foreach ($six_speed_records as $record) { // 设置每个资源的订单状态
$order_status[$record['resource_id']] = $record['is_closed'] ? '已开单' : '未开单'; foreach ($resource_ids as $resource_id) {
if (in_array($resource_id, $paid_resource_ids)) {
$order_status[$resource_id] = '已报名';
} else {
// 查找对应的六速记录
$six_speed_record = array_filter($six_speed_records, function($record) use ($resource_id) {
return $record['resource_id'] == $resource_id;
});
if (!empty($six_speed_record)) {
$six_speed_record = array_shift($six_speed_record);
$order_status[$resource_id] = $six_speed_record['is_closed'] ? '已开单' : '未开单';
} else {
$order_status[$resource_id] = '未报名';
}
}
} }
} }

89
niucloud/app/service/api/login/UnifiedLoginService.php

@ -18,6 +18,7 @@ use app\model\site\Site;
use app\model\customer_resources\CustomerResources; use app\model\customer_resources\CustomerResources;
use app\service\core\menu\CoreMenuService; use app\service\core\menu\CoreMenuService;
use core\util\TokenAuth; use core\util\TokenAuth;
use think\facade\Db;
use core\base\BaseService; use core\base\BaseService;
use core\exception\CommonException; use core\exception\CommonException;
use think\facade\Cache; use think\facade\Cache;
@ -437,46 +438,96 @@ class UnifiedLoginService extends BaseService
} }
/** /**
* 获取员工菜单列表 * 获取员工菜单列表(动态查询数据库)
* @param int $roleType * @param int $roleType
* @return array * @return array
*/ */
private function getStaffMenuList(int $roleType) private function getStaffMenuList(int $roleType)
{ {
// 根据角色类型返回对应的菜单权限 try {
// 查询角色对应的菜单权限
$menuList = Db::table('sys_menus')
->alias('m')
->leftJoin('role_menu_permissions rmp', 'm.id = rmp.menu_id')
->where('rmp.role_id', $roleType)
->where('rmp.is_enabled', 1)
->where('m.status', 1)
->field('m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order')
->order('m.sort_order ASC')
->select()
->toArray();
// 转换为前端需要的格式
$result = [];
foreach ($menuList as $menu) {
$menuItem = [
'key' => $menu['menu_key'],
'title' => $menu['menu_name'],
'icon' => $menu['menu_icon'],
'path' => $menu['menu_path'],
];
// 如果有参数,解析JSON参数
if (!empty($menu['menu_params'])) {
$params = json_decode($menu['menu_params'], true);
if ($params) {
$menuItem['params'] = $params;
}
}
$result[] = $menuItem;
}
return $result;
} catch (\Exception $e) {
// 如果数据库查询失败,返回默认菜单(兼容处理)
return $this->getDefaultStaffMenuList($roleType);
}
}
/**
* 获取默认员工菜单列表(兼容处理)
* @param int $roleType
* @return array
*/
private function getDefaultStaffMenuList(int $roleType)
{
// 根据角色类型返回对应的默认菜单权限
switch ($roleType) { switch ($roleType) {
case self::STAFF_ROLE_MARKET: case self::STAFF_ROLE_MARKET:
return [ return [
['path' => '/pages/market/home/index', 'name' => '首页', 'icon' => 'home'], ['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'],
['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], ['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'],
['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']],
['path' => '/pages/market/data/statistics', 'name' => '数据统计', 'icon' => 'data'], ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'],
['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], ['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'],
]; ];
case self::STAFF_ROLE_COACH: case self::STAFF_ROLE_COACH:
case self::STAFF_ROLE_TEACHER: case self::STAFF_ROLE_TEACHER:
return [ return [
['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], ['key' => 'course_query', 'title' => '课程查询', 'icon' => 'search', 'path' => '/pages-coach/coach/schedule/schedule_table'],
['path' => '/pages/coach/course/list', 'name' => '课表管理', 'icon' => 'course'], ['key' => 'student_management', 'title' => '学员管理', 'icon' => 'contact-filled', 'path' => '/pages-coach/coach/student/student_list'],
['path' => '/pages/coach/student/student_list', 'name' => '我的学员', 'icon' => 'student'], ['key' => 'course_arrangement', 'title' => '课程安排', 'icon' => 'calendar-filled', 'path' => '/pages-market/clue/class_arrangement'],
['path' => '/pages/coach/job/list', 'name' => '作业管理', 'icon' => 'job'], ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']],
['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'],
['key' => 'resource_library', 'title' => '资料库', 'icon' => 'folder-add-filled', 'path' => '/pages-coach/coach/my/teaching_management'],
]; ];
case self::STAFF_ROLE_SALES: case self::STAFF_ROLE_SALES:
return [ return [
['path' => '/pages/market/index/index', 'name' => '首页', 'icon' => 'home'], ['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'],
['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], ['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'],
['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']],
['path' => '/pages/market/clue/clue_table', 'name' => '数据统计', 'icon' => 'data'], ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'],
['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], ['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'],
]; ];
default: default:
return [ return [
['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']],
['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'],
]; ];
} }
} }

2
uniapp/pages-market/clue/class_arrangement.vue

@ -91,7 +91,7 @@
<view class="status-end">{{ getStatusText(course.status) }}</view> <view class="status-end">{{ getStatusText(course.status) }}</view>
</view> </view>
<view class="card-body"> <view class="card-body">
<view class="row">时间{{ course.course_date || '未设置' }}</view> <view class="row">时间{{ course.course_date || '未设置' }} {{ course.time_slot || '' }}</view>
<view class="row">校区{{ course.campus_name || '未设置' }}</view> <view class="row">校区{{ course.campus_name || '未设置' }}</view>
<view class="row">教室{{ course.venue ? course.venue.venue_name : '未设置' }}</view> <view class="row">教室{{ course.venue ? course.venue.venue_name : '未设置' }}</view>
<view class="row">课程{{ course.course ? course.course.course_name : '未设置' }}</view> <view class="row">课程{{ course.course ? course.course.course_name : '未设置' }}</view>

4
uniapp/pages-market/clue/index.vue

@ -45,8 +45,8 @@
<view :class="['status-tag',getOrderStatusClass(v.customerResource.order_status)]"> <view :class="['status-tag',getOrderStatusClass(v.customerResource.order_status)]">
{{ v.customerResource.order_status || '未报名' }} {{ v.customerResource.order_status || '未报名' }}
</view> </view>
<!-- 到访状态标签 --> <!-- 到访状态标签 - 只有未报名时才显示到访状态 -->
<view class="visit-status"> <view class="visit-status" v-if="(v.customerResource.order_status || '未报名') !== '已报名'">
<view :class="['visit-tag',getVisitStatusClass(v.customerResource.first_visit_status)]"> <view :class="['visit-tag',getVisitStatusClass(v.customerResource.first_visit_status)]">
一访{{ v.customerResource.first_visit_status || '未到' }} 一访{{ v.customerResource.first_visit_status || '未到' }}
</view> </view>

69
uniapp/pages/common/home/index.vue

@ -45,70 +45,29 @@
data() { data() {
return { return {
userInfo: {}, userInfo: {},
gridItems: [ gridItems: [],
{ defaultGridItems: [
title: '客户资源',
icon: 'person-filled',
path: '/pages-market/clue/index'
},
{
title: '添加资源',
icon: 'plus-filled',
path: '/pages-market/clue/add_clues'
},
{
title: '课程安排',
icon: 'calendar-filled',
path: '/pages-market/clue/class_arrangement'
},
{
title: '课程查询',
icon: 'search',
path: '/pages-coach/coach/schedule/schedule_table'
},
{
title: '学员管理',
icon: 'contact-filled',
path: '/pages-coach/coach/student/student_list'
},
{ {
title: '我的数据', title: '我的数据',
icon: 'bars', icon: 'bars',
path: '/pages/common/dashboard/webview', path: '/pages/common/dashboard/webview',
params: { type: 'my_data' } params: { type: 'my_data' }
}, },
{
title: '部门数据',
icon: 'staff',
path: '/pages/common/dashboard/webview',
params: { type: 'dept_data' }
},
{
title: '校区数据',
icon: 'location-filled',
path: '/pages/common/dashboard/webview',
params: { type: 'campus_data' }
},
{ {
title: '我的消息', title: '我的消息',
icon: 'chat-filled', icon: 'chat-filled',
path: '/pages-common/my_message' path: '/pages-common/my_message'
},
{
title: '报销管理',
icon: 'wallet-filled',
path: '/pages-market/reimbursement/list'
},
{
title: '资料库',
icon: 'folder-add-filled',
path: '/pages-coach/coach/my/teaching_management'
} }
] ]
} }
}, },
onLoad() { onLoad() {
this.loadUserInfo(); this.loadUserInfo();
this.loadMenuList();
},
onShow() {
//
this.loadMenuList();
}, },
methods: { methods: {
loadUserInfo() { loadUserInfo() {
@ -118,6 +77,20 @@
this.userInfo = userInfo; this.userInfo = userInfo;
} }
}, },
loadMenuList() {
//
const menuList = uni.getStorageSync('menuList');
console.log('从本地存储获取的菜单列表:', menuList);
if (menuList && Array.isArray(menuList) && menuList.length > 0) {
// 使
this.gridItems = menuList;
} else {
// 使
this.gridItems = this.defaultGridItems;
console.log('使用默认菜单');
}
},
handleGridClick(item) { handleGridClick(item) {
console.log('点击功能按钮:', item.title, item.path); console.log('点击功能按钮:', item.title, item.path);

69
uniapp/test-edit-clues.html

@ -1,69 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>测试编辑客户页面</title>
<meta charset="utf-8">
</head>
<body>
<h1>测试编辑客户页面字段回显</h1>
<h2>问题描述</h2>
<p>在 pages/market/clue/edit_clues 页面中,电话六要素的购买力字段和备注字段没有正确回显。</p>
<h2>问题原因</h2>
<ol>
<li><strong>购买力字段名不一致</strong>
<ul>
<li>前端使用:<code>purchasing_power_name</code></li>
<li>后端返回:<code>purchase_power_name</code></li>
<li>数据库字段:<code>purchase_power</code></li>
</ul>
</li>
<li><strong>备注字段名不一致</strong>
<ul>
<li>前端使用:<code>remark</code></li>
<li>数据库字段:<code>consultation_remark</code></li>
</ul>
</li>
</ol>
<h2>修复内容</h2>
<ol>
<li><strong>修复购买力字段</strong>
<ul>
<li>第875行:<code>purchasing_power: sixSpeed.purchase_power</code></li>
<li>第945行:<code>sixSpeed.purchase_power_name</code></li>
</ul>
</li>
<li><strong>修复备注字段</strong>
<ul>
<li>第886行:<code>remark: sixSpeed.consultation_remark</code></li>
</ul>
</li>
</ol>
<h2>测试数据</h2>
<p>已在数据库中为 resource_id=38 的记录设置测试数据:</p>
<ul>
<li>购买力:2(对应"中等")</li>
<li>备注:测试备注信息</li>
</ul>
<h2>验证步骤</h2>
<ol>
<li>打开页面:<code>pages/market/clue/edit_clues?resource_sharing_id=38</code></li>
<li>检查购买力选择器是否显示"中等"</li>
<li>检查备注输入框是否显示"测试备注信息"</li>
</ol>
<h2>修改的文件</h2>
<ul>
<li><code>uniapp/pages/market/clue/edit_clues.vue</code> - 修复字段名不一致问题</li>
</ul>
<script>
console.log('测试页面加载完成');
console.log('请在 UniApp 中测试编辑客户页面的字段回显功能');
</script>
</body>
</html>

268
uniapp/学员端订单接口问题修复报告.md

@ -1,268 +0,0 @@
# 学员端订单接口问题修复报告
## 🔍 **问题描述**
用户反馈学员端订单页面 `pages-student/orders/index` 不能调用 `api/xy/orderTable?student_id=31&page=1&limit=10` 这个接口。
## 🔧 **问题分析**
### **1. 原始问题**
通过 Playwright 测试发现以下问题:
#### **API调用失败**
```
[LOG] 调用get方法: {url: /xy/orderTable, data: Object}
[LOG] 响应拦截器处理: {statusCode: 200, data: Object}
[LOG] 业务状态码: 401
[ERROR] 401错误 - 未授权
[ERROR] 获取订单列表失败: Error: 请登录
```
#### **代码错误**
```
TypeError: _this.calculateOrderStats is not a function
```
### **2. 根本原因**
1. **权限问题**:`/xy/orderTable` 接口需要登录验证,返回401未授权错误
2. **方法缺失**:页面调用了不存在的 `calculateOrderStats()` 方法
3. **接口设计问题**:学员端使用的是需要管理员权限的接口
## ✅ **修复方案**
### **1. 修复代码错误**
```javascript
// 修复前(错误)
async initPage() {
await this.loadStudentInfo()
await this.loadOrders()
this.calculateOrderStats() // ❌ 方法不存在
}
// 修复后(正确)
async initPage() {
await this.loadStudentInfo()
await this.loadOrders()
this.updateOrderDisplay() // ✅ 使用正确的方法
}
```
### **2. 完善统计功能**
```javascript
updateOrderDisplay() {
// 更新过滤列表
if (this.activeStatus === 'all') {
this.filteredOrders = [...this.ordersList]
} else {
this.filteredOrders = this.ordersList.filter(order => order.status === this.activeStatus)
}
// 更新标签页统计
const counts = {}
this.ordersList.forEach(order => {
counts[order.status] = (counts[order.status] || 0) + 1
})
this.statusTabs.forEach(tab => {
tab.count = tab.value === 'all' ? this.ordersList.length : (counts[tab.value] || 0)
})
// ✅ 新增:更新订单统计信息
this.orderStats = {
total_orders: this.ordersList.length,
pending_payment: counts['pending_payment'] || 0,
paid: counts['paid'] || 0,
completed: counts['completed'] || 0,
cancelled: counts['cancelled'] || 0,
refunded: counts['refunded'] || 0
}
}
```
### **3. 创建学员端专用接口**
#### **在 apiRoute.js 中添加新接口**
```javascript
//学生端-订单管理-列表(公开接口,用于学员端查看)
async xy_getStudentOrders(data = {}) {
return await http.get('/xy/student/orders', data);
},
//学生端-订单管理-详情(公开接口,用于学员端查看)
async xy_getStudentOrderDetail(data = {}) {
return await http.get('/xy/student/orders/detail', data);
},
```
#### **更新页面调用**
```javascript
// 修复前(权限问题)
const response = await apiRoute.xy_orderTableList({
student_id: this.studentId,
page: this.currentPage,
limit: 10
})
// 修复后(使用学员端接口)
const response = await apiRoute.xy_getStudentOrders({
student_id: this.studentId,
page: this.currentPage,
limit: 10
})
```
## 🧪 **测试验证**
### **测试环境**
- **测试页面**:http://localhost:8080/#/pages-student/orders/index?student_id=31
- **测试工具**:Playwright 自动化测试
### **测试结果**
#### **修复前**
```
❌ API调用失败:401未授权错误
❌ 代码错误:calculateOrderStats is not a function
❌ 页面显示:获取订单列表失败
❌ 用户体验:功能不可用
```
#### **修复后**
```
✅ 代码错误已修复:不再有 calculateOrderStats 错误
✅ 新接口调用成功:/xy/student/orders 接口被正确调用
✅ 网络请求正常:HTTP 200 状态码
⚠️ 后端路由待实现:需要后端实现新的接口路由
```
### **网络请求日志**
```
[LOG] 调用学员端订单接口,参数: {student_id: 31, page: 1, limit: 10}
[LOG] 调用get方法: {url: /xy/student/orders, data: Object}
[GET] http://localhost:20080/api/xy/student/orders?student_id=31&page=1&limit=10 => [200] OK
[LOG] 业务状态码: 0 (路由未定义)
```
## 📋 **后端实现建议**
### **需要实现的接口**
#### **1. 学员订单列表接口**
```
GET /api/xy/student/orders
参数:
- student_id: 学员ID
- page: 页码
- limit: 每页数量
返回格式:
{
"code": 1,
"msg": "获取成功",
"data": {
"data": [
{
"id": 1,
"order_no": "ORD20250731001",
"course_name": "体能训练课程",
"total_amount": "299.00",
"status": "paid",
"create_time": "2025-07-31 10:00:00",
"payment_method": "wxpay"
}
],
"current_page": 1,
"last_page": 1,
"total": 1
}
}
```
#### **2. 学员订单详情接口**
```
GET /api/xy/student/orders/detail
参数:
- id: 订单ID
返回格式:
{
"code": 1,
"msg": "获取成功",
"data": {
"id": 1,
"order_no": "ORD20250731001",
"course_name": "体能训练课程",
"course_specs": "10节课",
"quantity": 1,
"total_amount": "299.00",
"status": "paid",
"create_time": "2025-07-31 10:00:00",
"payment_method": "wxpay",
"payment_time": "2025-07-31 10:05:00"
}
}
```
### **权限设计**
- 这些接口应该允许学员查看自己的订单
- 可以通过 `student_id` 参数限制只能查看自己的订单
- 不需要管理员权限,但需要验证学员身份
### **安全考虑**
- 验证 `student_id` 参数的有效性
- 确保学员只能查看自己的订单
- 添加适当的数据脱敏(如隐藏敏感支付信息)
## 🎯 **修复状态总结**
### **✅ 已完成**
1. **代码错误修复**:`calculateOrderStats` 方法调用错误已修复
2. **统计功能完善**:`orderStats` 数据正确更新
3. **前端接口调用**:已切换到新的学员端接口
4. **错误处理优化**:添加了详细的调试日志
### **⚠️ 待完成**
1. **后端接口实现**:需要实现 `/xy/student/orders``/xy/student/orders/detail` 接口
2. **权限验证**:后端需要实现适当的学员身份验证
3. **数据格式对接**:确保后端返回的数据格式与前端期望一致
### **🔄 下一步行动**
1. **后端开发**:实现新的学员端订单接口
2. **接口测试**:验证接口的功能和性能
3. **数据联调**:确保前后端数据格式一致
4. **权限测试**:验证学员只能查看自己的订单
## 💡 **技术亮点**
### **1. 接口分离设计**
- 将学员端和管理端的订单接口分离
- 学员端接口更简单,权限要求更低
- 便于后续的权限管理和功能扩展
### **2. 错误处理优化**
- 添加了详细的调试日志
- 改善了错误提示的用户体验
- 便于问题排查和调试
### **3. 代码健壮性提升**
- 修复了方法调用错误
- 完善了数据统计功能
- 提高了代码的可维护性
## 🎉 **总结**
通过系统性的分析和修复,成功解决了学员端订单页面的接口调用问题:
1. **✅ 问题定位准确**:识别出权限验证和代码错误问题
2. **✅ 修复方案合理**:创建专用的学员端接口
3. **✅ 代码质量提升**:修复了多个代码错误
4. **✅ 用户体验改善**:优化了错误处理和调试信息
5. **⚠️ 后端配合需要**:需要后端实现新的接口路由
**前端修复已完成,等待后端实现对应的接口即可完全解决问题!**
---
**修复完成时间**:2025-07-31
**状态**:✅ 前端修复完成,⚠️ 待后端实现接口
**新增接口**:`/xy/student/orders` 和 `/xy/student/orders/detail`
**下一步**:后端实现学员端订单接口
Loading…
Cancel
Save