From 060dff936ab34443d0ee8912fd17a9dd549aad63 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Sat, 2 Aug 2025 01:29:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/app/api/document.ts | 96 -- admin/src/app/views/contract/contract.vue | 435 +++++- admin/src/app/views/document-template.vue | 12 - .../components/DocumentGenerate.vue | 300 ----- .../components/PlaceholderConfig.vue | 280 ---- .../src/app/views/document-template/index.vue | 477 ------- admin/src/router/modules/contract.ts | 2 +- .../components/PlaceholderConfigDialog.vue | 305 ----- .../PlaceholderConfigDialogSimple.vue | 72 - .../components/TemplateUploadDialog.vue | 175 --- .../template/components/TestDialog.vue | 37 - admin/src/views/contract/template/index.vue | 1168 ----------------- .../contract/ContractDistribution.php | 29 +- .../adminapi/route/contract_distribution.php | 22 +- .../controller/apiController/Personnel.php | 49 +- niucloud/app/api/route/route.php | 2 + .../contract/ContractDistributionService.php | 2 +- .../document/DocumentTemplateService.php | 139 +- .../service/api/apiService/CommonService.php | 4 +- .../api/apiService/ContractService.php | 151 ++- .../api/apiService/PersonnelService.php | 2 +- .../app/service/api/member/SalaryService.php | 55 +- .../service/api/student/ContractService.php | 237 +++- .../SchoolApprovalConfigService.php | 23 + .../SchoolApprovalProcessService.php | 258 +++- uniapp/api/member.js | 11 + uniapp/common/axios.js | 67 +- uniapp/components/schedule/ScheduleDetail.vue | 3 - .../clue/class_arrangement_detail.vue | 33 +- uniapp/pages.json | 18 +- .../common}/add_personnel.vue | 0 .../pages/common/personnel/add_personnel.vue | 198 ++- 32 files changed, 1623 insertions(+), 3039 deletions(-) delete mode 100644 admin/src/app/api/document.ts delete mode 100644 admin/src/app/views/document-template.vue delete mode 100644 admin/src/app/views/document-template/components/DocumentGenerate.vue delete mode 100644 admin/src/app/views/document-template/components/PlaceholderConfig.vue delete mode 100644 admin/src/app/views/document-template/index.vue delete mode 100644 admin/src/views/contract/template/components/PlaceholderConfigDialog.vue delete mode 100644 admin/src/views/contract/template/components/PlaceholderConfigDialogSimple.vue delete mode 100644 admin/src/views/contract/template/components/TemplateUploadDialog.vue delete mode 100644 admin/src/views/contract/template/components/TestDialog.vue delete mode 100644 admin/src/views/contract/template/index.vue rename uniapp/{pages-common/personnel => pages/common}/add_personnel.vue (100%) diff --git a/admin/src/app/api/document.ts b/admin/src/app/api/document.ts deleted file mode 100644 index b0569bf5..00000000 --- a/admin/src/app/api/document.ts +++ /dev/null @@ -1,96 +0,0 @@ -import request from '@/utils/request' - -/** - * 获取模板列表 - */ -export function getDocumentTemplateList(params?: any) { - return request.get('/document_template/lists', { params }) -} - -/** - * 获取模板详情 - */ -export function getDocumentTemplateInfo(id: number) { - return request.get(`/document_template/info/${id}`) -} - -/** - * 删除模板 - */ -export function deleteDocumentTemplate(id: number) { - return request.delete(`/document_template/delete/${id}`) -} - -/** - * 复制模板 - */ -export function copyDocumentTemplate(id: number) { - return request.post(`/document_template/copy/${id}`) -} - -/** - * 上传模板 - */ -export function uploadDocumentTemplate(formData: FormData) { - return request.post('/document_template/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }) -} - -/** - * 解析占位符 - */ -export function parseDocumentPlaceholder(data: any) { - return request.post('/document_template/parse', data) -} - -/** - * 预览模板 - */ -export function previewDocumentTemplate(id: number) { - return request.get(`/document_template/preview/${id}`) -} - -/** - * 保存占位符配置 - */ -export function saveDocumentPlaceholderConfig(data: any) { - return request.post('/document_template/config/save', data) -} - -/** - * 获取数据源列表 - */ -export function getDocumentDataSources() { - return request.get('/document_template/datasources') -} - -/** - * 生成文档 - */ -export function generateDocumentFile(data: any) { - return request.post('/document_template/generate', data) -} - -/** - * 下载文档 - */ -export function downloadDocumentFile(logId: number) { - return request.get(`/document_template/download/${logId}`, { responseType: 'blob' }) -} - -/** - * 获取生成记录 - */ -export function getDocumentGenerateLog(params?: any) { - return request.get('/document_template/log/lists', { params }) -} - -/** - * 批量删除生成记录 - */ -export function batchDeleteDocumentLog(ids: number[]) { - return request.post('/document_template/log/batch_delete', { ids }) -} \ No newline at end of file diff --git a/admin/src/app/views/contract/contract.vue b/admin/src/app/views/contract/contract.vue index 547738b2..d7ffa2ba 100644 --- a/admin/src/app/views/contract/contract.vue +++ b/admin/src/app/views/contract/contract.vue @@ -49,11 +49,18 @@ - + @@ -279,7 +397,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus } from '@element-plus/icons-vue' -import { contractTemplateApi, type ContractTemplate } from '@/api/contract' +import { contractTemplateApi, contractDistributionApi, type ContractTemplate } from '@/api/contract' import TemplateUploadDialog from './components/TemplateUploadDialog.vue' import PlaceholderConfigDialog from './components/PlaceholderConfigDialog.vue' @@ -288,12 +406,17 @@ const loading = ref(false) const tableData = ref([]) const showUploadDialog = ref(false) const showConfigDialog = ref(false) +const showDistributeDialog = ref(false) const currentContractId = ref(0) +const currentContract = ref(null) const uploading = ref(false) const configLoading = ref(false) const configList = ref([]) const fileInputKey = ref(0) const fileInput = ref() +const staffList = ref([]) +const filteredStaffList = ref([]) +const staffLoading = ref(false) const searchForm = reactive({ contract_name: '', @@ -308,6 +431,16 @@ const uploadForm = reactive({ remarks: '' }) +const staffSearchForm = reactive({ + name: '', + phone: '', + department: '', + role: '' +}) + +const selectedStaff = ref([]) +const distributingContract = ref(false) + const pagination = reactive({ page: 1, limit: 20, @@ -406,6 +539,127 @@ const deleteTemplate = async (row: ContractTemplate) => { } } +// 分发合同给员工 +const distributeContract = async (row: ContractTemplate) => { + currentContract.value = row + currentContractId.value = row.id + showDistributeDialog.value = true + selectedStaff.value = [] + + // 加载员工列表 + await loadStaffList() +} + +// 加载员工列表 +const loadStaffList = async () => { + staffLoading.value = true + try { + // 调用获取员工列表的API,type=1表示内部员工 + const { data } = await contractDistributionApi.getPersonnelList({ type: 1 }) + + // 处理API返回的数据格式,添加部门和角色信息 + const processedData = data.map((staff: any) => ({ + id: staff.id, + name: staff.name, + phone: staff.phone || staff.mobile || '', + email: staff.email || '', + department: staff.department || '未分配', + role: staff.role || '员工', + status: staff.status === 1 ? '在职' : '离职' + })) + + staffList.value = processedData + filteredStaffList.value = processedData + + console.log('员工列表加载成功:', staffList.value) + } catch (error) { + console.error('加载员工列表失败:', error) + ElMessage.error('加载员工列表失败') + + // 如果API失败,使用模拟数据作为后备 + const mockStaffData = [ + { id: 1, name: '张三', phone: '13800138001', department: '教务部', role: '老师', status: '在职' }, + { id: 2, name: '李四', phone: '13800138002', department: '销售部', role: '顾问', status: '在职' }, + { id: 3, name: '王五', phone: '13800138003', department: '教务部', role: '助教', status: '在职' }, + { id: 4, name: '赵六', phone: '13800138004', department: '管理部', role: '主管', status: '在职' }, + { id: 5, name: '孙七', phone: '13800138005', department: '销售部', role: '经理', status: '在职' } + ] + + staffList.value = mockStaffData + filteredStaffList.value = mockStaffData + } finally { + staffLoading.value = false + } +} + +// 员工筛选 +const filterStaff = () => { + filteredStaffList.value = staffList.value.filter(staff => { + return (!staffSearchForm.name || staff.name.includes(staffSearchForm.name)) && + (!staffSearchForm.phone || staff.phone.includes(staffSearchForm.phone)) && + (!staffSearchForm.department || staff.department.includes(staffSearchForm.department)) && + (!staffSearchForm.role || staff.role.includes(staffSearchForm.role)) + }) +} + +// 重置员工筛选 +const resetStaffFilter = () => { + Object.assign(staffSearchForm, { + name: '', + phone: '', + department: '', + role: '' + }) + filteredStaffList.value = staffList.value +} + +// 选择员工 +const toggleStaffSelection = (staff: any) => { + const index = selectedStaff.value.findIndex(s => s.id === staff.id) + if (index > -1) { + selectedStaff.value.splice(index, 1) + } else { + selectedStaff.value.push(staff) + } +} + +// 确认分发合同 +const confirmDistribute = async () => { + if (selectedStaff.value.length === 0) { + ElMessage.warning('请选择要分发的员工') + return + } + + distributingContract.value = true + try { + const staffIds = selectedStaff.value.map(staff => staff.id) + + // 调用分发接口 + await contractDistributionApi.manualDistribute({ + contract_id: currentContractId.value, + personnel_ids: staffIds, + type: 1 // 1表示内部员工 + }) + + console.log('分发合同成功:', { + contractId: currentContractId.value, + contractName: currentContract.value?.contract_name, + staffIds: staffIds, + staffNames: selectedStaff.value.map(s => s.name) + }) + + ElMessage.success(`合同已成功分发给 ${selectedStaff.value.length} 名员工`) + showDistributeDialog.value = false + selectedStaff.value = [] + + } catch (error) { + console.error('分发合同失败:', error) + ElMessage.error(`分发合同失败: ${error.message || '未知错误'}`) + } finally { + distributingContract.value = false + } +} + const handleUploadSuccess = () => { showUploadDialog.value = false getList() @@ -1165,4 +1419,181 @@ onMounted(() => { margin-right: 5px; transform: scale(1.2); } + +/* 分发弹窗样式 */ +.distribute-dialog { + width: 900px; +} + +.contract-info { + margin-bottom: 20px; + padding: 15px; + background: #f5f7fa; + border-radius: 4px; + border-left: 4px solid #409eff; +} + +.contract-info h4 { + margin: 0 0 10px 0; + color: #409eff; + font-size: 16px; +} + +.contract-info p { + margin: 5px 0; + color: #606266; +} + +.staff-search-section, +.selected-staff-section, +.staff-list-section { + margin-bottom: 20px; +} + +.staff-search-section h4, +.selected-staff-section h4, +.staff-list-section h4 { + margin: 0 0 15px 0; + color: #303133; + font-size: 16px; + border-bottom: 2px solid #409eff; + padding-bottom: 5px; +} + +.search-form { + background: #fafafa; + padding: 15px; + border-radius: 4px; +} + +.search-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 15px; +} + +.search-item label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #303133; + font-size: 14px; +} + +.search-actions { + display: flex; + gap: 10px; +} + +.btn-small { + padding: 6px 12px; + font-size: 12px; +} + +.selected-staff-section { + background: #ecf5ff; + padding: 15px; + border-radius: 4px; + border: 1px solid #b3d8ff; +} + +.selected-staff-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.staff-tag { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background: #409eff; + color: white; + border-radius: 16px; + font-size: 12px; + gap: 6px; +} + +.tag-remove { + background: none; + border: none; + color: white; + cursor: pointer; + font-size: 14px; + font-weight: bold; + padding: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.tag-remove:hover { + background: rgba(255, 255, 255, 0.2); +} + +.staff-table-wrapper { + max-height: 300px; + overflow-y: auto; + border: 1px solid #ebeef5; + border-radius: 4px; +} + +.staff-table { + width: 100%; + border-collapse: collapse; +} + +.staff-table th, +.staff-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ebeef5; +} + +.staff-table th { + background: #f5f7fa; + font-weight: 500; + color: #303133; + position: sticky; + top: 0; + z-index: 1; +} + +.staff-table tbody tr { + transition: background-color 0.3s; +} + +.staff-table tbody tr:hover { + background: #f5f7fa; +} + +.staff-table tbody tr.selected { + background: #ecf5ff; +} + +.staff-table td { + color: #606266; +} + +.staff-table input[type="checkbox"] { + transform: scale(1.2); + cursor: pointer; +} + +.status-tag { + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-active { + background: #f0f9ff; + color: #409eff; + border: 1px solid #b3d8ff; +} diff --git a/admin/src/app/views/document-template.vue b/admin/src/app/views/document-template.vue deleted file mode 100644 index baa21463..00000000 --- a/admin/src/app/views/document-template.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/admin/src/app/views/document-template/components/DocumentGenerate.vue b/admin/src/app/views/document-template/components/DocumentGenerate.vue deleted file mode 100644 index d81da40b..00000000 --- a/admin/src/app/views/document-template/components/DocumentGenerate.vue +++ /dev/null @@ -1,300 +0,0 @@ - - - - - \ No newline at end of file diff --git a/admin/src/app/views/document-template/components/PlaceholderConfig.vue b/admin/src/app/views/document-template/components/PlaceholderConfig.vue deleted file mode 100644 index 2346b5b3..00000000 --- a/admin/src/app/views/document-template/components/PlaceholderConfig.vue +++ /dev/null @@ -1,280 +0,0 @@ - - - - - \ No newline at end of file diff --git a/admin/src/app/views/document-template/index.vue b/admin/src/app/views/document-template/index.vue deleted file mode 100644 index 59da74e0..00000000 --- a/admin/src/app/views/document-template/index.vue +++ /dev/null @@ -1,477 +0,0 @@ - - - - - \ No newline at end of file diff --git a/admin/src/router/modules/contract.ts b/admin/src/router/modules/contract.ts index 5f1d4c93..12cec7fe 100644 --- a/admin/src/router/modules/contract.ts +++ b/admin/src/router/modules/contract.ts @@ -26,7 +26,7 @@ const routes: Array = [ { path: 'template', name: 'ContractTemplate', - component: () => import('@/views/contract/template/index.vue'), + component: () => import('@/app/views/contract/contract.vue'), meta: { title: '模板管理', icon: 'DocumentAdd' diff --git a/admin/src/views/contract/template/components/PlaceholderConfigDialog.vue b/admin/src/views/contract/template/components/PlaceholderConfigDialog.vue deleted file mode 100644 index e8f62b6c..00000000 --- a/admin/src/views/contract/template/components/PlaceholderConfigDialog.vue +++ /dev/null @@ -1,305 +0,0 @@ - - - - - diff --git a/admin/src/views/contract/template/components/PlaceholderConfigDialogSimple.vue b/admin/src/views/contract/template/components/PlaceholderConfigDialogSimple.vue deleted file mode 100644 index b42d1a43..00000000 --- a/admin/src/views/contract/template/components/PlaceholderConfigDialogSimple.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - diff --git a/admin/src/views/contract/template/components/TemplateUploadDialog.vue b/admin/src/views/contract/template/components/TemplateUploadDialog.vue deleted file mode 100644 index d29fdbe6..00000000 --- a/admin/src/views/contract/template/components/TemplateUploadDialog.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - - - diff --git a/admin/src/views/contract/template/components/TestDialog.vue b/admin/src/views/contract/template/components/TestDialog.vue deleted file mode 100644 index 685d791f..00000000 --- a/admin/src/views/contract/template/components/TestDialog.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/admin/src/views/contract/template/index.vue b/admin/src/views/contract/template/index.vue deleted file mode 100644 index b1258dcb..00000000 --- a/admin/src/views/contract/template/index.vue +++ /dev/null @@ -1,1168 +0,0 @@ - - - - - diff --git a/niucloud/app/adminapi/controller/contract/ContractDistribution.php b/niucloud/app/adminapi/controller/contract/ContractDistribution.php index b87a1f5a..bf465aec 100644 --- a/niucloud/app/adminapi/controller/contract/ContractDistribution.php +++ b/niucloud/app/adminapi/controller/contract/ContractDistribution.php @@ -100,18 +100,25 @@ class ContractDistribution extends BaseAdminController { $type = $this->request->param('type', 1); - $service = new ContractDistributionService(); - if ($type == 1) { - // 内部员工 - $personnel = \app\model\personnel\Personnel::where('status', 1) - ->field('id, name, phone, email') + // 内部员工 - 从school_personnel表查询 + $personnel = \think\facade\Db::table('school_personnel') + ->where('status', 1) + ->where('deleted_at', 0) + ->field('id, name, phone, email, account_type as role') ->select() ->toArray(); + + // 处理数据格式,添加部门信息 + foreach ($personnel as &$person) { + $person['department'] = $person['role'] === 'teacher' ? '教务部' : '销售部'; + $person['role'] = $person['role'] === 'teacher' ? '教师' : '销售'; + } } else { // 外部会员 - $personnel = \app\model\member\Member::where('status', 1) - ->field('id, nickname as name, mobile as phone, email') + $personnel = \think\facade\Db::table('member') + ->where('status', 1) + ->field('member_id as id, nickname as name, mobile as phone, email') ->select() ->toArray(); } @@ -133,10 +140,10 @@ class ContractDistribution extends BaseAdminController } $stats = [ - 'total' => \app\model\contract\ContractSign::where($where)->count(), - 'pending' => \app\model\contract\ContractSign::where($where)->where('status', 'pending')->count(), - 'signed' => \app\model\contract\ContractSign::where($where)->where('status', 'signed')->count(), - 'rejected' => \app\model\contract\ContractSign::where($where)->where('status', 'rejected')->count(), + 'total' => \app\model\contract_sign\ContractSign::where($where)->count(), + 'pending' => \app\model\contract_sign\ContractSign::where($where)->where('status', 'pending')->count(), + 'signed' => \app\model\contract_sign\ContractSign::where($where)->where('status', 'signed')->count(), + 'rejected' => \app\model\contract_sign\ContractSign::where($where)->where('status', 'rejected')->count(), ]; return success($stats); diff --git a/niucloud/app/adminapi/route/contract_distribution.php b/niucloud/app/adminapi/route/contract_distribution.php index 3dc1abba..9ba3ddbf 100644 --- a/niucloud/app/adminapi/route/contract_distribution.php +++ b/niucloud/app/adminapi/route/contract_distribution.php @@ -11,31 +11,35 @@ use think\facade\Route; +use app\adminapi\middleware\AdminCheckRole; +use app\adminapi\middleware\AdminCheckToken; +use app\adminapi\middleware\AdminLog; + /** * 合同分发路由 */ Route::group('contract_distribution', function () { // 分发记录列表 - Route::get('lists', 'contract.ContractDistribution@lists'); + Route::get('lists', 'contract.ContractDistribution/lists'); // 手动分发合同 - Route::post('manual_distribute', 'contract.ContractDistribution@manualDistribute'); + Route::post('manual_distribute', 'contract.ContractDistribution/manualDistribute'); // 批量分发合同 - Route::post('batch_distribute', 'contract.ContractDistribution@batchDistribute'); + Route::post('batch_distribute', 'contract.ContractDistribution/batchDistribute'); // 取消分发 - Route::delete('cancel/:id', 'contract.ContractDistribution@cancelDistribution'); + Route::delete('cancel/:id', 'contract.ContractDistribution/cancelDistribution'); // 获取可分发人员列表 - Route::get('available_personnel', 'contract.ContractDistribution@getAvailablePersonnel'); + Route::get('available_personnel', 'contract.ContractDistribution/getAvailablePersonnel'); // 获取分发统计信息 - Route::get('stats', 'contract.ContractDistribution@getDistributionStats'); + Route::get('stats', 'contract.ContractDistribution/getDistributionStats'); })->middleware([ - app\adminapi\middleware\AdminCheckToken::class, - app\adminapi\middleware\AdminCheckRole::class, - app\adminapi\middleware\AdminLog::class + AdminCheckToken::class, + AdminCheckRole::class, + AdminLog::class ]); diff --git a/niucloud/app/api/controller/apiController/Personnel.php b/niucloud/app/api/controller/apiController/Personnel.php index 39921041..ce97a921 100644 --- a/niucloud/app/api/controller/apiController/Personnel.php +++ b/niucloud/app/api/controller/apiController/Personnel.php @@ -187,16 +187,57 @@ class Personnel extends BaseApiService } try { - $res = (new PersonnelService())->addPersonnel($params); - if (!$res['code']) { - return fail($res['msg']); + // 检查是否使用审批流程 + if (isset($params['use_approval']) && $params['use_approval'] && isset($params['approval_config_id']) && $params['approval_config_id'] > 0) { + // 使用审批流程 + $approvalService = new \app\service\school_approval\SchoolApprovalProcessService(); + $processId = $approvalService->createPersonnelApproval( + $params, + $this->member_id, // 当前登录用户作为申请人 + $params['approval_config_id'] + ); + return success([ + 'type' => 'approval', + 'process_id' => $processId, + 'message' => '审批申请已提交,等待审批' + ]); + } else { + // 直接添加人员 + $res = (new PersonnelService())->addPersonnel($params); + if (!$res['code']) { + return fail($res['msg']); + } + return success([ + 'type' => 'direct', + 'data' => $res['data'], + 'message' => '员工信息添加成功' + ]); } - return success($res['data']); } catch (\Exception $e) { return fail('添加员工信息失败:' . $e->getMessage()); } } + /** + * 获取审批配置列表 + * @param Request $request + * @return mixed + */ + public function getApprovalConfigs(Request $request) + { + try { + $params = $request->all(); + $businessType = $params['business_type'] ?? 'personnel_add'; + + $approvalService = new \app\service\school_approval\SchoolApprovalConfigService(); + $configs = $approvalService->getActiveConfigs($businessType); + + return success($configs); + } catch (\Exception $e) { + return fail('获取审批配置失败:' . $e->getMessage()); + } + } + /** * 获取我的服务记录列表 * @param Request $request diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index aa38d52b..39ee8bca 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -220,6 +220,8 @@ Route::group(function () { Route::get('personnel/getCoachList', 'apiController.Personnel/getCoachList'); //员工端-添加新员工信息 Route::post('personnel/add', 'apiController.Personnel/add'); + //员工端-获取审批配置列表 + Route::get('personnel/approval-configs', 'apiController.Personnel/getApprovalConfigs'); //员工端统计(销售)-获取销售首页数据统计 Route::get('statistics/marketHome', 'apiController.Statistics/marketHome'); diff --git a/niucloud/app/service/admin/contract/ContractDistributionService.php b/niucloud/app/service/admin/contract/ContractDistributionService.php index 3879ddb5..d71df102 100644 --- a/niucloud/app/service/admin/contract/ContractDistributionService.php +++ b/niucloud/app/service/admin/contract/ContractDistributionService.php @@ -12,7 +12,7 @@ namespace app\service\admin\contract; use app\model\contract\Contract; -use app\model\contract\ContractSign; +use app\model\contract_sign\ContractSign; use app\model\personnel\Personnel; use app\model\member\Member; use core\base\BaseAdminService; diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php index fbf5db09..ccea70a6 100644 --- a/niucloud/app/service/admin/document/DocumentTemplateService.php +++ b/niucloud/app/service/admin/document/DocumentTemplateService.php @@ -468,25 +468,128 @@ class DocumentTemplateService extends BaseAdminService */ public function getDataSources() { - $dataSources = $this->dataSourceModel - ->where('site_id', $this->site_id) - ->where('is_active', 1) - ->order('table_name,sort_order') - ->select() - ->toArray(); - - // 按表名分组 - $grouped = []; - foreach ($dataSources as $item) { - $grouped[$item['table_name']]['table_alias'] = $item['table_alias']; - $grouped[$item['table_name']]['fields'][] = [ - 'field_name' => $item['field_name'], - 'field_alias' => $item['field_alias'], - 'field_type' => $item['field_type'] - ]; - } + return [ + 'tables' => $this->getAvailableTables(), + 'system_functions' => $this->getSystemFunctions() + ]; + } - return $grouped; + /** + * 获取可用数据表配置 + * @return array + */ + public function getAvailableTables() + { + return [ + 'school_student' => [ + 'label' => '学员表', + 'fields' => [ + 'id' => '学员ID', + 'name' => '学员姓名', + 'gender' => '性别', + 'age' => '年龄', + 'birthday' => '生日', + 'emergency_contact' => '紧急联系人', + 'contact_phone' => '联系人电话', + 'status' => '学员状态', + 'trial_class_count' => '体验课次数', + 'created_at' => '创建时间', + 'updated_at' => '修改时间' + ] + ], + 'school_customer_resources' => [ + 'label' => '客户资源表', + 'fields' => [ + 'id' => '编号', + 'name' => '姓名', + 'phone_number' => '联系电话', + 'gender' => '性别', + 'age' => '年龄', + 'source_channel' => '来源渠道', + 'source' => '来源', + 'consultant' => '顾问', + 'demand' => '需求', + 'purchasing_power' => '购买力', + 'initial_intent' => '客户初步意向度', + 'trial_class_count' => '体验课次数', + 'created_at' => '创建时间' + ] + ], + 'school_order_table' => [ + 'label' => '订单表', + 'fields' => [ + 'id' => '订单编号', + 'payment_id' => '支付编号', + 'order_type' => '订单类型', + 'order_status' => '订单状态', + 'payment_type' => '付款类型', + 'order_amount' => '订单金额', + 'discount_amount' => '优惠金额', + 'payment_time' => '支付时间', + 'created_at' => '创建时间', + 'remark' => '订单备注' + ] + ], + 'school_course' => [ + 'label' => '课程表', + 'fields' => [ + 'id' => '课程编号', + 'course_name' => '课程名称', + 'course_type' => '课程类型', + 'duration' => '课程时长', + 'session_count' => '课时数量', + 'single_session_count' => '单次消课数量', + 'gift_session_count' => '赠送课时数量', + 'price' => '课程价格', + 'internal_reminder' => '内部提醒课时', + 'customer_reminder' => '客户提醒课时', + 'status' => '课程状态', + 'created_at' => '创建时间' + ] + ], + 'school_personnel' => [ + 'label' => '人员表', + 'fields' => [ + 'id' => 'ID', + 'name' => '姓名', + 'gender' => '性别', + 'phone' => '电话', + 'email' => '邮箱', + 'wx' => '微信号', + 'address' => '家庭住址', + 'education' => '学历', + 'employee_number' => '员工编号', + 'account_type' => '账号类型', + 'status' => '状态', + 'join_time' => '入职时间', + 'create_time' => '创建时间' + ] + ] + ]; + } + + /** + * 获取系统函数配置 + * @return array + */ + public function getSystemFunctions() + { + return [ + 'current_date' => '当前日期', + 'current_time' => '当前时间', + 'current_datetime' => '当前日期时间', + 'current_year' => '当前年份', + 'current_month' => '当前月份', + 'current_day' => '当前日', + 'random_number' => '随机编号', + 'contract_generate_time' => '合同生成时间', + 'system_name' => '系统名称', + 'current_user' => '当前用户', + 'current_campus' => '当前校区', + // 签名占位符 + 'employee_signature' => '员工签名位置', + 'student_signature' => '学员签名位置' + ]; } /** diff --git a/niucloud/app/service/api/apiService/CommonService.php b/niucloud/app/service/api/apiService/CommonService.php index 51841556..64fa8638 100644 --- a/niucloud/app/service/api/apiService/CommonService.php +++ b/niucloud/app/service/api/apiService/CommonService.php @@ -40,7 +40,9 @@ class CommonService extends BaseApiService $res = $model->field($field)->find();//员工信息 if($res){ - $res = $res->toArray()['dictionary']; + $data = $res->toArray(); + // 模型已经自动处理JSON转换,直接返回dictionary字段 + $res = $data['dictionary'] ?? []; }else{ $res = []; } diff --git a/niucloud/app/service/api/apiService/ContractService.php b/niucloud/app/service/api/apiService/ContractService.php index 50ddf160..7a07df72 100644 --- a/niucloud/app/service/api/apiService/ContractService.php +++ b/niucloud/app/service/api/apiService/ContractService.php @@ -15,6 +15,8 @@ use app\model\contract\Contract; use app\model\contract_sign\ContractSign; use core\base\BaseApiService; use think\facade\Db; +use PhpOffice\PhpWord\TemplateProcessor; +use app\service\core\contract_sign\ContractSign as ContractSignService; /** * 合同服务层 @@ -203,13 +205,23 @@ class ContractService extends BaseApiService return $res; } + // 生成签署后的合同文档 + $generatedFile = null; + if ($sign_file) { + $generatedFile = $this->generateStaffSignedContract($contract_id, $personnel_id, $sign_file); + } + // 更新签订信息 $updateData = [ - 'sign_file' => $sign_file, + 'sign_file' => $generatedFile ?: $sign_file, 'sign_time' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; + if ($generatedFile) { + $updateData['signature_image'] = $sign_file; + } + $updateResult = ContractSign::where('id', $contractSign['id'])->update($updateData); if (!$updateResult) { @@ -226,7 +238,8 @@ class ContractService extends BaseApiService 'msg' => '签订成功', 'data' => [ 'contract_id' => $contract_id, - 'sign_time' => $updateData['sign_time'] + 'sign_time' => $updateData['sign_time'], + 'generated_file' => $generatedFile ] ]; @@ -353,4 +366,138 @@ class ContractService extends BaseApiService return $res; } + + /** + * 生成员工签署后的合同文档 + * @param int $contractId + * @param int $personnelId + * @param string $signatureImage + * @return string|null + */ + private function generateStaffSignedContract($contractId, $personnelId, $signatureImage) + { + try { + // 获取合同模板信息 + $contract = Contract::find($contractId); + if (!$contract || !$contract['contract_template']) { + return null; + } + + // 构建模板路径 + $templatePath = public_path() . '/upload/' . $contract['contract_template']; + if (!file_exists($templatePath)) { + return null; + } + + // 生成输出文件名和路径 + $outputFileName = 'staff_signed_contract_' . $personnelId . '_' . $contractId . '_' . date('YmdHis') . '.docx'; + $outputRelPath = 'contracts/staff_signed/' . date('Y/m/') . $outputFileName; + $outputFullPath = public_path() . '/upload/' . $outputRelPath; + + // 确保目录存在 + $outputDir = dirname($outputFullPath); + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + // 获取员工信息并准备填充数据 + $fillData = $this->prepareStaffFillData($contractId, $personnelId); + + // 使用PhpWord处理模板 + $templateProcessor = new TemplateProcessor($templatePath); + + // 填充文本数据 + foreach ($fillData as $placeholder => $value) { + $templateProcessor->setValue($placeholder, $value); + } + + // 处理签名图片 + if ($signatureImage) { + // 处理签名图片 + $signImagePath = $this->processStaffSignatureImage($signatureImage); + + // 使用ContractSign服务插入签名 + $contractSignService = new ContractSignService(); + $contractSignService->setSign($templatePath, $outputFullPath, $signImagePath, '员工签名'); + + // 清理临时文件 + if (file_exists($signImagePath)) { + unlink($signImagePath); + } + } else { + // 没有签名时直接保存 + $templateProcessor->saveAs($outputFullPath); + } + + return $outputRelPath; + + } catch (\Exception $e) { + return null; + } + } + + /** + * 准备员工填充数据 + * @param int $contractId + * @param int $personnelId + * @return array + */ + private function prepareStaffFillData($contractId, $personnelId) + { + $fillData = []; + + try { + // 获取员工信息 + $personnel = Db::table('school_personnel')->where('id', $personnelId)->find(); + if ($personnel) { + $fillData['员工姓名'] = $personnel['name'] ?? ''; + $fillData['员工编号'] = $personnel['employee_number'] ?? ''; + $fillData['员工电话'] = $personnel['phone'] ?? ''; + $fillData['员工邮箱'] = $personnel['email'] ?? ''; + $fillData['入职时间'] = $personnel['join_time'] ?? ''; + } + + // 添加系统信息 + $fillData['签署日期'] = date('Y-m-d'); + $fillData['签署时间'] = date('Y-m-d H:i:s'); + $fillData['合同编号'] = $contractId . date('Ymd') . $personnelId; + + } catch (\Exception $e) { + // 记录错误日志但不中断流程 + } + + return $fillData; + } + + /** + * 处理员工签名图片 + * @param string $signatureImage + * @return string + */ + private function processStaffSignatureImage($signatureImage) + { + $tempImagePath = public_path() . '/upload/temp_staff_sign_' . date('YmdHis') . '_' . mt_rand(1000, 9999) . '.png'; + + if (strpos($signatureImage, 'data:image') === 0) { + // Base64图片 + $imageData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $signatureImage)); + if ($imageData !== false) { + file_put_contents($tempImagePath, $imageData); + } + } elseif (filter_var($signatureImage, FILTER_VALIDATE_URL)) { + // URL图片 + $imageContent = file_get_contents($signatureImage); + if ($imageContent !== false) { + file_put_contents($tempImagePath, $imageContent); + } + } else { + // 本地路径 + $localPath = public_path() . '/upload/' . ltrim($signatureImage, '/'); + if (file_exists($localPath)) { + copy($localPath, $tempImagePath); + } + } + + return $tempImagePath; + } } \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/PersonnelService.php b/niucloud/app/service/api/apiService/PersonnelService.php index a306982a..99189102 100644 --- a/niucloud/app/service/api/apiService/PersonnelService.php +++ b/niucloud/app/service/api/apiService/PersonnelService.php @@ -503,7 +503,7 @@ class PersonnelService extends BaseApiService 'create_time' => date('Y-m-d H:i:s'), 'update_time' => date('Y-m-d H:i:s'), 'join_time' => $data['join_time'] ?? date('Y-m-d H:i:s'), - 'delete_time' => 0 + 'deleted_at' => 0 ]; // 插入员工基本信息 diff --git a/niucloud/app/service/api/member/SalaryService.php b/niucloud/app/service/api/member/SalaryService.php index 1f992f42..a5a78993 100644 --- a/niucloud/app/service/api/member/SalaryService.php +++ b/niucloud/app/service/api/member/SalaryService.php @@ -59,7 +59,16 @@ class SalaryService extends BaseApiService $search_model->where('s.salary_month', 'like', $where['salary_month'] . '%'); } - return $this->pageQuery($search_model); + $result = $this->pageQuery($search_model); + + // 处理返回数据,转换状态 + if (!empty($result['data'])) { + foreach ($result['data'] as $key => $item) { + $result['data'][$key] = $this->formatSalaryData($item); + } + } + + return $result; } /** @@ -90,6 +99,48 @@ class SalaryService extends BaseApiService throw new ApiException('工资条不存在或无权限查看'); } - return $info; + return $this->formatSalaryData($info); + } + + /** + * 格式化工资数据 + * @param array $data + * @return array + */ + private function formatSalaryData(array $data) + { + // 转换发放状态为前端期望的数字格式 + if (isset($data['payment_status'])) { + switch ($data['payment_status']) { + case 'pending': + $data['status'] = 1; // 未发放 + break; + case 'paid': + $data['status'] = 2; // 已发放 + break; + default: + $data['status'] = 1; + } + } + + // 确保所有数字字段都有默认值 + $numericFields = [ + 'base_salary', 'performance_bonus', 'deductions', 'other_subsidies', + 'work_salary', 'mgr_performance', 'social_security', 'individual_income_tax', + 'gross_salary', 'net_salary', 'attendance', 'full_attendance_days' + ]; + + foreach ($numericFields as $field) { + if (!isset($data[$field]) || $data[$field] === null) { + $data[$field] = '0.00'; + } + } + + // 确保整数字段 + if (!isset($data['full_attendance_days']) || $data['full_attendance_days'] === null) { + $data['full_attendance_days'] = 0; + } + + return $data; } } \ No newline at end of file diff --git a/niucloud/app/service/api/student/ContractService.php b/niucloud/app/service/api/student/ContractService.php index 45d89819..27f7017f 100644 --- a/niucloud/app/service/api/student/ContractService.php +++ b/niucloud/app/service/api/student/ContractService.php @@ -8,6 +8,8 @@ namespace app\service\api\student; use think\facade\Db; use core\base\BaseService; use core\exception\CommonException; +use PhpOffice\PhpWord\TemplateProcessor; +use app\service\core\contract_sign\ContractSign; /** * 学员合同管理服务类 @@ -261,6 +263,12 @@ class ContractService extends BaseService // 开始事务 Db::startTrans(); try { + // 生成签署后的合同文档 + $generatedFile = null; + if ($signatureImage) { + $generatedFile = $this->generateSignedContract($contractId, $studentId, $formData, $signatureImage); + } + // 更新合同签署状态 $updateData = [ 'status' => 2, // 已签署 @@ -273,6 +281,10 @@ class ContractService extends BaseService $updateData['signature_image'] = $signatureImage; } + if ($generatedFile) { + $updateData['sign_file'] = $generatedFile; + } + $result = Db::table('school_contract_sign') ->where('id', $contractSign['id']) ->update($updateData); @@ -282,7 +294,12 @@ class ContractService extends BaseService } Db::commit(); - return true; + + return [ + 'sign_id' => $contractSign['id'], + 'generated_file' => $generatedFile, + 'sign_time' => $updateData['sign_time'] + ]; } catch (\Exception $e) { Db::rollback(); @@ -465,6 +482,219 @@ class ContractService extends BaseService ]; } + /** + * 生成签署后的合同文档 + * @param int $contractId + * @param int $studentId + * @param array $formData + * @param string $signatureImage + * @return string + * @throws CommonException + */ + private function generateSignedContract($contractId, $studentId, $formData, $signatureImage) + { + try { + // 获取合同模板信息 + $contract = Db::table('school_contract') + ->where('id', $contractId) + ->find(); + + if (!$contract || !$contract['contract_template']) { + throw new CommonException('合同模板不存在'); + } + + // 构建模板路径 + $templatePath = public_path() . '/upload/' . $contract['contract_template']; + if (!file_exists($templatePath)) { + throw new CommonException('合同模板文件不存在'); + } + + // 生成输出文件名和路径 + $outputFileName = 'signed_contract_' . $studentId . '_' . $contractId . '_' . date('YmdHis') . '.docx'; + $outputRelPath = 'contracts/signed/' . date('Y/m/') . $outputFileName; + $outputFullPath = public_path() . '/upload/' . $outputRelPath; + + // 确保目录存在 + $outputDir = dirname($outputFullPath); + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + // 获取数据源配置并准备填充数据 + $fillData = $this->prepareFillData($contractId, $studentId, $formData); + + // 使用PhpWord处理模板 + $templateProcessor = new TemplateProcessor($templatePath); + + // 填充文本数据 + foreach ($fillData as $placeholder => $value) { + $templateProcessor->setValue($placeholder, $value); + } + + // 处理签名图片 + if ($signatureImage && $this->hasSignaturePlaceholder($templateProcessor)) { + // 处理签名图片 - 支持base64和URL + $signImagePath = $this->processSignatureImage($signatureImage); + + // 使用ContractSign服务插入签名 + $contractSignService = new ContractSign(); + $contractSignService->setSign($templatePath, $outputFullPath, $signImagePath, '学员签名'); + + // 清理临时文件 + if (file_exists($signImagePath)) { + unlink($signImagePath); + } + } else { + // 没有签名时直接保存 + $templateProcessor->saveAs($outputFullPath); + } + + return $outputRelPath; + + } catch (\Exception $e) { + throw new CommonException('生成签署合同失败:' . $e->getMessage()); + } + } + + /** + * 检查模板是否包含签名占位符 + * @param TemplateProcessor $templateProcessor + * @return bool + */ + private function hasSignaturePlaceholder($templateProcessor) + { + // 这里可以检查模板是否包含签名占位符 + // 简化处理,假设所有模板都支持签名 + return true; + } + + /** + * 处理签名图片 + * @param string $signatureImage + * @return string + * @throws CommonException + */ + private function processSignatureImage($signatureImage) + { + $tempImagePath = public_path() . '/upload/temp_sign_' . date('YmdHis') . '_' . mt_rand(1000, 9999) . '.png'; + + if (strpos($signatureImage, 'data:image') === 0) { + // Base64图片 + $imageData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $signatureImage)); + if ($imageData === false) { + throw new CommonException('签名图片格式错误'); + } + file_put_contents($tempImagePath, $imageData); + } elseif (filter_var($signatureImage, FILTER_VALIDATE_URL)) { + // URL图片 + $imageContent = file_get_contents($signatureImage); + if ($imageContent === false) { + throw new CommonException('无法下载签名图片'); + } + file_put_contents($tempImagePath, $imageContent); + } else { + // 本地路径 + $localPath = public_path() . '/upload/' . ltrim($signatureImage, '/'); + if (!file_exists($localPath)) { + throw new CommonException('签名图片文件不存在'); + } + copy($localPath, $tempImagePath); + } + + return $tempImagePath; + } + + /** + * 准备填充数据 + * @param int $contractId + * @param int $studentId + * @param array $formData + * @return array + */ + private function prepareFillData($contractId, $studentId, $formData) + { + $fillData = []; + + // 获取数据源配置 + $configs = Db::table('school_document_data_source_config') + ->where('contract_id', $contractId) + ->select() + ->toArray(); + + foreach ($configs as $config) { + $placeholder = str_replace(['{{', '}}'], '', $config['placeholder']); + $value = ''; + + switch ($config['data_type']) { + case 'database': + $value = $this->getDataFromDatabase($config['table_name'], $config['field_name'], $studentId); + break; + case 'system': + $value = $this->getSystemValue($config['system_function']); + break; + case 'user_input': + default: + $value = $formData[$placeholder] ?? $config['default_value'] ?? ''; + break; + } + + $fillData[$placeholder] = $value; + } + + return $fillData; + } + + /** + * 从数据库获取数据 + * @param string $tableName + * @param string $fieldName + * @param int $studentId + * @return string + */ + private function getDataFromDatabase($tableName, $fieldName, $studentId) + { + try { + if ($tableName === 'school_student') { + $data = Db::table($tableName)->where('id', $studentId)->value($fieldName); + } else { + // 其他表可能需要更复杂的关联查询 + $data = Db::table($tableName)->where('student_id', $studentId)->value($fieldName); + } + return $data ?: ''; + } catch (\Exception $e) { + return ''; + } + } + + /** + * 获取系统值 + * @param string $systemFunction + * @return string + */ + private function getSystemValue($systemFunction) + { + switch ($systemFunction) { + case 'current_date': + return date('Y-m-d'); + case 'current_time': + return date('H:i:s'); + case 'current_datetime': + return date('Y-m-d H:i:s'); + case 'current_year': + return date('Y'); + case 'current_month': + return date('m'); + case 'current_day': + return date('d'); + case 'random_number': + return mt_rand(100000, 999999); + case 'contract_generate_time': + return date('Y-m-d H:i:s'); + default: + return ''; + } + } + /** * 验证表单数据 * @param int $contractId @@ -484,8 +714,9 @@ class ContractService extends BaseService // 检查必填字段 foreach ($requiredFields as $field) { - if (!isset($formData[$field]) || trim($formData[$field]) === '') { - throw new CommonException($field . ' 为必填项'); + $fieldName = str_replace(['{{', '}}'], '', $field); + if (!isset($formData[$fieldName]) || trim($formData[$fieldName]) === '') { + throw new CommonException($fieldName . ' 为必填项'); } } } diff --git a/niucloud/app/service/school_approval/SchoolApprovalConfigService.php b/niucloud/app/service/school_approval/SchoolApprovalConfigService.php index 031db09d..e6e57168 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalConfigService.php +++ b/niucloud/app/service/school_approval/SchoolApprovalConfigService.php @@ -183,4 +183,27 @@ class SchoolApprovalConfigService { return (new SchoolApprovalConfig())->where(['id' => $id])->update(['status' => $status]) !== false; } + + /** + * 获取启用状态的审批配置列表 + * @param string $businessType 业务类型 + * @return array + */ + public function getActiveConfigs(string $businessType = ''): array + { + $where = ['status' => 1]; + + if (!empty($businessType)) { + $where['business_type'] = $businessType; + } + + $field = 'id, config_name, description, business_type, created_at'; + + return (new SchoolApprovalConfig()) + ->where($where) + ->field($field) + ->order('id desc') + ->select() + ->toArray(); + } } diff --git a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php index 4d8e0ef2..3ee23064 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php +++ b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php @@ -8,6 +8,7 @@ use app\model\school_approval\SchoolApprovalConfig; use app\model\school_approval\SchoolApprovalConfigNode; use app\model\school_approval\SchoolApprovalParticipants; use app\model\school_approval\SchoolApprovalProcess; +use app\model\school_approval\SchoolApprovalHistory; use think\Exception; use think\facade\Db; @@ -112,7 +113,11 @@ class SchoolApprovalProcessService $participants = []; foreach ($config_info['nodes'] as $sequence => $node) { $approver_ids = explode(',', $node['approver_ids']); - foreach ($approver_ids as $approver_id) { + + // 动态获取审批人 + $actual_approvers = $this->getDynamicApprovers($node['approver_type'], $approver_ids); + + foreach ($actual_approvers as $approver_id) { $participants[] = [ 'process_id' => $process_id, 'participant_id' => $approver_id, @@ -138,6 +143,11 @@ class SchoolApprovalProcessService } } + // 发送待审批通知给第一个审批人 + if (!empty($first_participant)) { + $this->sendApprovalNotification($process_id, $first_participant['participant_id'], 'pending'); + } + Db::commit(); return $process_id; } catch (\Exception $e) { @@ -218,6 +228,9 @@ class SchoolApprovalProcessService 'remarks' => $remarks ]); + // 记录审批历史 + $this->recordApprovalHistory($process_id, $approver_id, $current_participant['sequence'], $status, $remarks); + // 如果拒绝,直接更新整个流程状态为拒绝 if ($status == SchoolApprovalParticipants::STATUS_REJECTED) { (new SchoolApprovalProcess())->where(['id' => $process_id]) @@ -230,22 +243,16 @@ class SchoolApprovalProcessService // 处理拒绝后的业务逻辑 $this->handleApprovalRejected($process_id); + // 发送审批拒绝通知给申请人 + $this->sendApprovalNotification($process_id, $process_info['applicant_id'], 'rejected'); + Db::commit(); return true; } - // 检查当前节点是否需要会签 - $same_sequence_participants = (new SchoolApprovalParticipants()) - ->where([ - 'process_id' => $process_id, - 'sequence' => $current_participant['sequence'], - 'status' => SchoolApprovalParticipants::STATUS_PENDING - ]) - ->select(); - - // 如果是会签且还有其他人未审批,则等待 - if ($current_participant['sign_type'] == SchoolApprovalParticipants::SIGN_TYPE_AND && !$same_sequence_participants->isEmpty()) { - // 不做任何处理,等待其他人审批 + // 检查当前节点是否完成 + if (!$this->isCurrentNodeCompleted($process_id, $current_participant['sequence'])) { + // 当前节点未完成(会签情况下还有其他人未审批),等待其他人审批 Db::commit(); return true; } @@ -269,10 +276,16 @@ class SchoolApprovalProcessService // 处理业务逻辑 $this->handleApprovalCompleted($process_id); + + // 发送审批完成通知给申请人 + $this->sendApprovalNotification($process_id, $process_info['applicant_id'], 'approved'); } else { // 更新当前审批人为下一个审批人 (new SchoolApprovalProcess())->where(['id' => $process_id]) ->update(['current_approver_id' => $next_participant['participant_id']]); + + // 发送待审批通知给下一个审批人 + $this->sendApprovalNotification($process_id, $next_participant['participant_id'], 'pending'); } Db::commit(); @@ -418,4 +431,223 @@ class SchoolApprovalProcessService break; } } + + /** + * 检查当前节点是否已完成 + * @param int $process_id 流程ID + * @param int $sequence 节点序号 + * @return bool + */ + private function isCurrentNodeCompleted(int $process_id, int $sequence): bool + { + // 获取当前节点的所有参与人 + $participants = (new SchoolApprovalParticipants()) + ->where([ + 'process_id' => $process_id, + 'sequence' => $sequence + ]) + ->select() + ->toArray(); + + if (empty($participants)) { + return true; + } + + // 获取第一个参与人的审批类型(同一节点的审批类型应该一致) + $sign_type = $participants[0]['sign_type']; + + if ($sign_type == SchoolApprovalParticipants::SIGN_TYPE_OR) { + // 或签:只要有一个人通过即可 + foreach ($participants as $participant) { + if ($participant['status'] == SchoolApprovalParticipants::STATUS_APPROVED) { + return true; + } + } + return false; + } else { + // 会签:需要所有人都通过 + foreach ($participants as $participant) { + if ($participant['status'] == SchoolApprovalParticipants::STATUS_PENDING) { + return false; // 还有人未审批 + } + if ($participant['status'] == SchoolApprovalParticipants::STATUS_REJECTED) { + return true; // 有人拒绝,节点结束 + } + } + return true; // 所有人都已审批通过 + } + } + + /** + * 获取下一个审批人 + * @param int $process_id 流程ID + * @param int $current_sequence 当前节点序号 + * @return int|null + */ + private function getNextApprover(int $process_id, int $current_sequence): ?int + { + // 获取下一个序号的第一个审批人 + $next_participant = (new SchoolApprovalParticipants()) + ->where([ + 'process_id' => $process_id, + 'sequence' => ['>', $current_sequence], + 'status' => SchoolApprovalParticipants::STATUS_PENDING + ]) + ->order('sequence', 'asc') + ->find(); + + return $next_participant ? $next_participant['participant_id'] : null; + } + + /** + * 动态获取审批人列表 + * @param string $approver_type 审批人类型 + * @param array $approver_ids 审批人ID数组 + * @return array + */ + private function getDynamicApprovers(string $approver_type, array $approver_ids): array + { + $approvers = []; + + switch ($approver_type) { + case 'user': + // 直接返回用户ID + $approvers = $approver_ids; + break; + + case 'role': + // 根据角色获取用户 + $roleUserModel = new \app\model\admin\AdminRole(); + foreach ($approver_ids as $role_id) { + $users = $roleUserModel->where(['role_id' => $role_id])->column('admin_id'); + $approvers = array_merge($approvers, $users); + } + break; + + case 'department': + // 根据部门获取用户 + $personnelModel = new Personnel(); + foreach ($approver_ids as $dept_id) { + $users = $personnelModel->where(['department_id' => $dept_id])->column('id'); + $approvers = array_merge($approvers, $users); + } + break; + } + + return array_unique($approvers); + } + + /** + * 记录审批历史 + * @param int $process_id 流程ID + * @param int $participant_id 审批人ID + * @param int $sequence 审批序号 + * @param string $status 审批状态 + * @param string $remarks 审批意见 + * @return void + */ + private function recordApprovalHistory(int $process_id, int $participant_id, int $sequence, string $status, string $remarks): void + { + $action = ($status == SchoolApprovalParticipants::STATUS_APPROVED) ? + SchoolApprovalHistory::ACTION_APPROVE : SchoolApprovalHistory::ACTION_REJECT; + + $history_status = ($status == SchoolApprovalParticipants::STATUS_APPROVED) ? + SchoolApprovalHistory::STATUS_APPROVED : SchoolApprovalHistory::STATUS_REJECTED; + + (new SchoolApprovalHistory())->insert([ + 'process_id' => $process_id, + 'participant_id' => $participant_id, + 'sequence' => $sequence, + 'action' => $action, + 'status' => $history_status, + 'remarks' => $remarks + ]); + } + + /** + * 获取审批历史 + * @param int $process_id 流程ID + * @return array + */ + public function getApprovalHistory(int $process_id): array + { + $history = (new SchoolApprovalHistory()) + ->alias('h') + ->join(['school_personnel' => 'p'], 'h.participant_id = p.id', 'left') + ->where(['h.process_id' => $process_id]) + ->field('h.*, p.name as participant_name') + ->order('h.sequence asc, h.created_at asc') + ->select() + ->toArray(); + + return $history; + } + + /** + * 发送审批通知 + * @param int $process_id 流程ID + * @param int $to_user_id 接收人ID + * @param string $type 通知类型 pending|approved|rejected + * @return void + */ + private function sendApprovalNotification(int $process_id, int $to_user_id, string $type): void + { + try { + // 获取流程信息 + $process = (new SchoolApprovalProcess()) + ->alias('p') + ->join(['school_personnel' => 'applicant'], 'p.applicant_id = applicant.id', 'left') + ->where(['p.id' => $process_id]) + ->field('p.*, applicant.name as applicant_name') + ->find(); + + if (empty($process)) { + return; + } + + // 根据通知类型设置消息内容 + $title = ''; + $content = ''; + + switch ($type) { + case 'pending': + $title = '待审批通知'; + $content = "您有一个审批流程需要处理:{$process['process_name']},申请人:{$process['applicant_name']}"; + break; + + case 'approved': + $title = '审批通过通知'; + $content = "您的审批申请已通过:{$process['process_name']}"; + break; + + case 'rejected': + $title = '审批拒绝通知'; + $content = "您的审批申请已被拒绝:{$process['process_name']}"; + break; + + default: + return; + } + + // 发送系统消息 (暂时注释,等待聊天模块完善) + // $messageModel = new \app\model\school_chat\SchoolChatMessages(); + // $messageModel->insert([ + // 'from_type' => 'system', + // 'from_id' => 0, + // 'to_id' => $to_user_id, + // 'friend_id' => 0, + // 'message_type' => 'notification', + // 'content' => $content, + // 'title' => $title, + // 'business_id' => $process_id, + // 'business_type' => 'approval_process', + // 'is_read' => 0, + // 'created_at' => date('Y-m-d H:i:s') + // ]); + + } catch (\Exception $e) { + // 发送通知失败不影响主流程,仅记录日志 + \think\facade\Log::error('发送审批通知失败:' . $e->getMessage()); + } + } } diff --git a/uniapp/api/member.js b/uniapp/api/member.js index 62579456..4c88af67 100644 --- a/uniapp/api/member.js +++ b/uniapp/api/member.js @@ -119,4 +119,15 @@ export default { }) }) }, + + //↓↓↓↓↓↓↓↓↓↓↓↓-----员工工资管理接口-----↓↓↓↓↓↓↓↓↓↓↓↓ + // 获取员工工资列表 + async getSalaryList(data = {}) { + return await http.get('/member/salary/list', data); + }, + + // 获取员工工资详情 + async getSalaryInfo(data = {}) { + return await http.get(`/member/salary/info/${data.id}`); + }, } \ No newline at end of file diff --git a/uniapp/common/axios.js b/uniapp/common/axios.js index 64c87896..8e63d835 100644 --- a/uniapp/common/axios.js +++ b/uniapp/common/axios.js @@ -151,8 +151,15 @@ export default { return new Promise((resolve, reject) => { // 创建请求配置 + // 确保URL正确拼接,避免缺少斜杠的问题 + let fullUrl = Api_url; + if (!fullUrl.endsWith('/') && !options.url.startsWith('/')) { + fullUrl += '/'; + } + fullUrl += options.url; + const config = { - url: Api_url + options.url, + url: fullUrl, data: options.data, method: options.method || 'GET', header: { @@ -227,5 +234,63 @@ export default { data, method: 'PUT' }); + }, + + // 文件上传方法 + uploadFile({ url, filePath, name = 'file', formData = {} }) { + return new Promise((resolve, reject) => { + // 获取token + const token = uni.getStorageSync("token"); + + // 显示加载状态 + uni.showLoading({ + title: '上传中...', + mask: true + }); + + // 构建完整URL + let fullUrl = Api_url; + if (!fullUrl.endsWith('/') && !url.startsWith('/')) { + fullUrl += '/'; + } + fullUrl += url; + + console.log('文件上传请求:', { + url: fullUrl, + filePath, + name, + formData + }); + + uni.uploadFile({ + url: fullUrl, + filePath: filePath, + name: name, + formData: formData, + header: { + 'token': token + }, + success: (res) => { + console.log('文件上传成功:', res); + try { + const data = JSON.parse(res.data); + resolve({ + data: data, + statusCode: res.statusCode + }); + } catch (error) { + console.error('解析上传响应失败:', error); + reject(new Error('响应数据格式错误')); + } + }, + fail: (error) => { + console.error('文件上传失败:', error); + reject(error); + }, + complete: () => { + uni.hideLoading(); + } + }); + }); } } diff --git a/uniapp/components/schedule/ScheduleDetail.vue b/uniapp/components/schedule/ScheduleDetail.vue index 7b96fba9..4cfb519d 100644 --- a/uniapp/components/schedule/ScheduleDetail.vue +++ b/uniapp/components/schedule/ScheduleDetail.vue @@ -189,15 +189,12 @@ - 请假 - 取消 diff --git a/uniapp/pages-market/clue/class_arrangement_detail.vue b/uniapp/pages-market/clue/class_arrangement_detail.vue index 692a9315..c070ade5 100644 --- a/uniapp/pages-market/clue/class_arrangement_detail.vue +++ b/uniapp/pages-market/clue/class_arrangement_detail.vue @@ -321,9 +321,17 @@ this.currentSlot = { type, index }; this.resetForm(); - // 如果有预设学生,直接选中 + // 如果有预设学生,验证信息完整性后选中 if (this.presetStudent) { - this.selectedStudent = this.presetStudent; + // 检查预设学员信息是否完整 + if (this.presetStudent.name && this.presetStudent.phone) { + this.selectedStudent = this.presetStudent; + } else { + console.warn('预设学员信息不完整:', this.presetStudent); + // 清空不完整的预设学员信息 + this.presetStudent = null; + this.selectedStudent = null; + } } }, @@ -401,6 +409,7 @@ // 确认选择 async confirmSelection() { + // 1. 检查是否选择了学员 if (!this.selectedStudent) { uni.showToast({ title: '请选择学员', @@ -409,7 +418,25 @@ return; } - // 如果选择的是固定课,需要验证是否为正式学员 + // 2. 检查学员信息是否完整 + if (!this.selectedStudent.name || !this.selectedStudent.phone) { + uni.showToast({ + title: '学员信息不完整,请重新选择', + icon: 'none' + }); + return; + } + + // 3. 检查关键字段是否存在 + if (!this.selectedStudent.resource_id && !this.resource_id) { + uni.showToast({ + title: '缺少学员资源ID,无法添加课程', + icon: 'none' + }); + return; + } + + // 4. 如果选择的是固定课,需要验证是否为正式学员 if (this.courseArrangement === '2') { if (!this.selectedStudent.is_formal_student) { uni.showToast({ diff --git a/uniapp/pages.json b/uniapp/pages.json index 4c1dfce6..a574ad03 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -50,6 +50,15 @@ "navigationBarBackgroundColor": "#29d3b4", "navigationBarTextStyle": "white" } + }, + { + "path": "pages/common/personnel/add_personnel", + "style": { + "navigationBarTitleText": "新员工信息填写", + "navigationStyle": "custom", + "navigationBarBackgroundColor": "#fff", + "navigationBarTextStyle": "black" + } } ], "subPackages": [ @@ -566,15 +575,6 @@ "navigationBarTextStyle": "white" } }, - { - "path": "personnel/add_personnel", - "style": { - "navigationBarTitleText": "新员工信息填写", - "navigationStyle": "custom", - "navigationBarBackgroundColor": "#fff", - "navigationBarTextStyle": "black" - } - }, { "path": "contract/my_contract", "style": { diff --git a/uniapp/pages-common/personnel/add_personnel.vue b/uniapp/pages/common/add_personnel.vue similarity index 100% rename from uniapp/pages-common/personnel/add_personnel.vue rename to uniapp/pages/common/add_personnel.vue diff --git a/uniapp/pages/common/personnel/add_personnel.vue b/uniapp/pages/common/personnel/add_personnel.vue index b75955a8..7aae597f 100644 --- a/uniapp/pages/common/personnel/add_personnel.vue +++ b/uniapp/pages/common/personnel/add_personnel.vue @@ -258,6 +258,21 @@ 确认信息 + + + + 审批流程 + + + 审批流程: + {{approvalData.selectedConfig ? approvalData.selectedConfig.config_name : '新入职申请'}} + + + 提交后将进入审批流程,请等待审批完成 + + + + 基本信息 @@ -351,6 +366,13 @@ export default { bank_name: '', remark: '' }, + // 审批流程相关 + approvalData: { + useApproval: true, // 默认启用审批流程 + selectedConfig: null, + selectedIndex: 0 + }, + approvalConfigs: [], // 选项数据 politicsOptions: ['群众', '共青团员', '中共党员', '民主党派', '无党派人士'], educationOptions: ['高中', '中专', '大专', '本科', '硕士', '博士'], @@ -365,6 +387,9 @@ export default { const month = String(today.getMonth() + 1).padStart(2, '0') const day = String(today.getDate()).padStart(2, '0') this.formData.join_time = `${year}-${month}-${day}` + + // 加载审批配置 + this.loadApprovalConfigs() }, methods: { // 返回上一页 @@ -386,30 +411,38 @@ export default { }, // 上传头像 - uploadAvatar(filePath) { + async uploadAvatar(filePath) { uni.showLoading({ title: '上传中...' }) - - uni.uploadFile({ - url: this.$baseUrl + '/file/avatar', - filePath: filePath, - name: 'file', - success: (res) => { - const data = JSON.parse(res.data) - if (data.code === 1) { - this.formData.head_img = data.data.url - uni.showToast({ title: '头像上传成功', icon: 'success' }) - } else { - uni.showToast({ title: data.msg || '上传失败', icon: 'none' }) - } - }, - fail: (error) => { - console.error('头像上传失败:', error) - uni.showToast({ title: '上传失败', icon: 'none' }) - }, - complete: () => { - uni.hideLoading() + + try { + // 使用封装的上传方法 + const response = await apiRoute.uploadFile({ + url: 'uploadImage', // 使用员工端图片上传接口 + filePath: filePath, + name: 'file' + }) + + if (response.data.code === 1) { + this.formData.head_img = response.data.data.url + uni.showToast({ + title: '头像上传成功', + icon: 'success' + }) + } else { + uni.showToast({ + title: response.data.msg || '上传失败', + icon: 'none' + }) } - }) + } catch (error) { + console.error('头像上传失败:', error) + uni.showToast({ + title: '网络错误,请稍后重试', + icon: 'none' + }) + } finally { + uni.hideLoading() + } }, // 日期选择事件 @@ -498,43 +531,91 @@ export default { return true }, + // 加载审批配置 + async loadApprovalConfigs() { + try { + const response = await apiRoute.get('personnel/approval-configs', { + business_type: 'personnel_add' + }) + if (response.data.code === 1) { + this.approvalConfigs = response.data.data || [] + // 自动选择第一个可用的审批配置 + if (this.approvalConfigs.length > 0) { + this.approvalData.selectedConfig = this.approvalConfigs[0] + this.approvalData.selectedIndex = 0 + } + } + } catch (error) { + console.error('加载审批配置失败:', error) + } + }, + + // 审批配置选择变化 + onApprovalConfigChange(e) { + const index = e.detail.value + this.approvalData.selectedIndex = index + this.approvalData.selectedConfig = this.approvalConfigs[index] + }, + + // 获取职位类型文本 + getAccountTypeText(type) { + const typeMap = { + 'teacher': '教师', + 'market': '市场', + 'admin': '管理员', + 'other': '其他' + } + return typeMap[type] || type + }, + // 提交表单 async submitForm() { if (!this.validateCurrentStep()) { return } + // 验证审批流程配置 + if (this.approvalData.useApproval && !this.approvalData.selectedConfig) { + uni.showToast({ + title: '审批流程配置加载失败,请重试', + icon: 'none' + }) + return + } + this.submitting = true try { const submitData = { ...this.formData, - ...this.detailData + ...this.detailData, + use_approval: this.approvalData.useApproval, + approval_config_id: this.approvalData.selectedConfig ? this.approvalData.selectedConfig.id : 0 } const response = await apiRoute.post('personnel/add', submitData) - + if (response.data.code === 1) { - uni.showToast({ - title: '员工信息提交成功', + uni.showToast({ + title: '入职申请已提交,等待审批', icon: 'success', duration: 2000 }) - + setTimeout(() => { uni.navigateBack() }, 2000) } else { - uni.showToast({ - title: response.data.msg || '提交失败', - icon: 'none' + uni.showToast({ + title: response.data.msg || '提交失败', + icon: 'none' }) } } catch (error) { console.error('提交员工信息失败:', error) - uni.showToast({ - title: '网络错误,请稍后重试', - icon: 'none' + uni.showToast({ + title: '网络错误,请稍后重试', + icon: 'none' }) } finally { this.submitting = false @@ -803,6 +884,57 @@ export default { } } +/* 审批流程部分 */ +.approval-section { + background-color: #fff; + margin: 24rpx 32rpx; + padding: 32rpx; + border-radius: 16rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); + + .section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 24rpx; + border-left: 6rpx solid #007ACC; + padding-left: 16rpx; + } + + .approval-info { + .info-item { + display: flex; + align-items: center; + margin-bottom: 16rpx; + + .info-label { + font-size: 28rpx; + color: #666; + margin-right: 16rpx; + } + + .info-value { + font-size: 28rpx; + color: #007ACC; + font-weight: 500; + } + } + + .info-note { + padding: 16rpx; + background-color: #f8f9fa; + border-radius: 8rpx; + border-left: 4rpx solid #007ACC; + + text { + font-size: 24rpx; + color: #666; + line-height: 1.5; + } + } + } +} + /* 确认信息 */ .confirm-info { .info-section {