From 92eb9f623d3c1400837515b7ca0679cc3a5456e7 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Tue, 29 Jul 2025 15:34:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/mock/salary.js | 167 +++++ admin/src/app/api/salary.ts | 151 +++-- ...salary-edit.vue => salary-edit.vue.backup} | 0 admin/src/app/views/salary/detail.vue | 407 +++++++++++++ admin/src/app/views/salary/edit.vue | 565 +++++++++++++++++ admin/src/app/views/salary/list.vue | 420 +++++++++++++ .../salary/{salary.vue => salary.vue.backup} | 0 admin/src/app/views/salary/statistics.vue | 576 ++++++++++++++++++ admin/src/router/modules/salary.ts | 56 ++ doc/劳 动 合 同.docx | Bin 0 -> 40961 bytes .../adminapi/controller/salary/Payroll.php | 132 ++++ .../adminapi/controller/salary/Statistics.php | 52 ++ niucloud/app/adminapi/route/salary.php | 16 + .../app/adminapi/validate/salary/Payroll.php | 59 ++ niucloud/app/model/salary/Salary.php | 6 +- .../service/admin/salary/PayrollService.php | 199 ++++++ .../admin/salary/StatisticsService.php | 117 ++++ uniapp/api/member.js | 16 + uniapp/pages.json | 9 + uniapp/pages/coach/my/salary.vue | 546 +++++++++++++++++ uniapp/pages/common/profile/index.vue | 8 +- 开发任务分配和质量控制.md | 305 ++++++++++ 项目开发管理方案.md | 422 +++++++++++++ 23 files changed, 4178 insertions(+), 51 deletions(-) create mode 100644 admin/mock/salary.js rename admin/src/app/views/salary/components/{salary-edit.vue => salary-edit.vue.backup} (100%) create mode 100644 admin/src/app/views/salary/detail.vue create mode 100644 admin/src/app/views/salary/edit.vue create mode 100644 admin/src/app/views/salary/list.vue rename admin/src/app/views/salary/{salary.vue => salary.vue.backup} (100%) create mode 100644 admin/src/app/views/salary/statistics.vue create mode 100644 admin/src/router/modules/salary.ts create mode 100644 doc/劳 动 合 同.docx create mode 100644 niucloud/app/adminapi/controller/salary/Payroll.php create mode 100644 niucloud/app/adminapi/controller/salary/Statistics.php create mode 100644 niucloud/app/adminapi/validate/salary/Payroll.php create mode 100644 niucloud/app/service/admin/salary/PayrollService.php create mode 100644 niucloud/app/service/admin/salary/StatisticsService.php create mode 100644 uniapp/pages/coach/my/salary.vue create mode 100644 开发任务分配和质量控制.md create mode 100644 项目开发管理方案.md diff --git a/admin/mock/salary.js b/admin/mock/salary.js new file mode 100644 index 00000000..90f61ff9 --- /dev/null +++ b/admin/mock/salary.js @@ -0,0 +1,167 @@ +import Mock from 'mockjs' + +// 工资条列表Mock +Mock.mock(/\/adminapi\/salary\/payroll\/list/, 'get', { + code: 1, + msg: '操作成功', + data: { + 'list|10': [{ + 'id|+1': 1, + 'staff_id|1-100': 1, + 'staff_name': '@cname', + 'campus_name|1': ['海淀校区', '朝阳校区', '丰台校区'], + 'salary_month': '2025-01', + 'base_salary|3000-8000.2': 5000, + 'full_attendance_days|20-24': 22, + 'attendance|15-22.1': 20, + 'work_salary|2000-7000.2': 4545.45, + 'mgr_performance|0-1000.2': 500, + 'performance_bonus|0-2000.2': 1000, + 'other_subsidies|0-500.2': 200, + 'deductions|0-200.2': 0, + 'gross_salary|4000-10000.2': 6245.45, + 'social_security|500-1200.2': 800, + 'individual_income_tax|0-500.2': 125, + 'net_salary|3000-9000.2': 5320.45, + 'status|0-2': 1, + 'created_at': '@datetime' + }], + total: 156, + page: 1, + limit: 10 + } +}) + +// 工资条详情Mock +Mock.mock(/\/adminapi\/salary\/payroll\/info/, 'get', { + code: 1, + msg: '操作成功', + data: { + id: '@id', + staff_id: '@integer(1, 100)', + staff_name: '@cname', + campus_name: '海淀校区', + salary_month: '2025-01', + base_salary: 6000.00, + full_attendance_days: 22, + attendance: 20.5, + work_salary: 5590.91, + mgr_performance: 800.00, + performance_bonus: 1200.00, + other_subsidies: 300.00, + deductions: 100.00, + gross_salary: 7790.91, + social_security: 960.00, + individual_income_tax: 285.00, + net_salary: 6545.91, + status: 1, + remarks: '本月表现优秀,给予额外奖励', + created_at: '@datetime', + updated_at: '@datetime' + } +}) + +// 创建工资条Mock +Mock.mock(/\/adminapi\/salary\/payroll\/add/, 'post', { + code: 1, + msg: '添加成功', + data: { + id: '@id' + } +}) + +// 更新工资条Mock +Mock.mock(/\/adminapi\/salary\/payroll\/edit/, 'post', { + code: 1, + msg: '更新成功', + data: null +}) + +// 删除工资条Mock +Mock.mock(/\/adminapi\/salary\/payroll\/delete/, 'post', { + code: 1, + msg: '删除成功', + data: null +}) + +// 导入工资条Mock +Mock.mock(/\/adminapi\/salary\/payroll\/import/, 'post', { + code: 1, + msg: '导入成功', + data: { + success_count: 25, + error_count: 2, + error_list: [ + { row: 3, error: '员工不存在' }, + { row: 8, error: '校区信息错误' } + ] + } +}) + +// 导出工资条Mock +Mock.mock(/\/adminapi\/salary\/payroll\/export/, 'get', { + code: 1, + msg: '导出成功', + data: 'blob_data_here' +}) + +// 统计摘要Mock +Mock.mock(/\/adminapi\/salary\/statistics\/summary/, 'get', { + code: 1, + msg: '操作成功', + data: { + total_staff: 65, + total_amount: 445480.70, + average_salary: 6853.55, + cost_rate: 78.5 + } +}) + +// 趋势数据Mock +Mock.mock(/\/adminapi\/salary\/statistics\/trend/, 'get', { + code: 1, + msg: '操作成功', + data: { + 'months|12': [{ + 'month': '@date("yyyy-MM")', + 'total_amount|30000-50000.2': 40000, + 'average_salary|6000-8000.2': 7000, + 'staff_count|50-80': 65 + }] + } +}) + +// 员工列表Mock +Mock.mock(/\/adminapi\/personnel\/list/, 'get', { + code: 1, + msg: '操作成功', + data: { + 'list|50': [{ + 'id|+1': 1, + 'name': '@cname', + 'campus_id|1-3': 1, + 'campus_name|1': ['海淀校区', '朝阳校区', '丰台校区'], + 'department': '@ctitle(2, 4)', + 'position': '@ctitle(3, 6)', + 'status|0-1': 1 + }] + } +}) + +// 校区列表Mock +Mock.mock(/\/adminapi\/campus\/list/, 'get', { + code: 1, + msg: '操作成功', + data: { + 'list|5': [{ + 'id|+1': 1, + 'name|1': ['海淀校区', '朝阳校区', '丰台校区', '昌平校区', '大兴校区'], + 'address': '@county(true)', + 'manager': '@cname', + 'phone': /^1[3-9]\d{9}$/, + 'status|0-1': 1 + }] + } +}) + +export default {} \ No newline at end of file diff --git a/admin/src/app/api/salary.ts b/admin/src/app/api/salary.ts index 5a942e1d..46359e1f 100644 --- a/admin/src/app/api/salary.ts +++ b/admin/src/app/api/salary.ts @@ -1,71 +1,132 @@ import request from '@/utils/request' +// 工资条数据类型 +export interface SalaryItem { + id: number + staff_id: number + staff_name: string + campus_name: string + salary_month: string + base_salary: number + full_attendance_days: number + attendance: number + work_salary: number + mgr_performance: number + performance_bonus: number + other_subsidies: number + deductions: number + gross_salary: number + social_security: number + individual_income_tax: number + net_salary: number + status: number + created_at: string +} +// 表单数据类型 +export interface SalaryFormData { + staff_id?: number + campus_id?: number + salary_month?: string + base_salary: number + full_attendance_days: number + attendance: number + mgr_performance: number + performance_bonus: number + other_subsidies: number + deductions: number + social_security: number + individual_income_tax: number + remarks?: string +} +// 查询参数类型 +export interface QueryParams { + page: number + limit: number + campus_id?: number + salary_month?: string + staff_name?: string + status?: number +} +// 统计数据类型 +export interface StatisticsSummary { + total_staff: number + total_amount: number + average_salary: number + cost_rate: number +} +// 工资计算逻辑函数 +export const calculateWorkSalary = (baseSalary: number, fullDays: number, attendance: number): number => { + if (!baseSalary || !fullDays) return 0 + return Number(((baseSalary / fullDays) * attendance).toFixed(2)) +} +export const calculateGrossSalary = (workSalary: number, mgr: number, bonus: number, subsidies: number, deductions: number): number => { + return Number((workSalary + mgr + bonus + subsidies - deductions).toFixed(2)) +} +export const calculateNetSalary = (grossSalary: number, socialSecurity: number, tax: number): number => { + return Number((grossSalary - socialSecurity - tax).toFixed(2)) +} - -// USER_CODE_BEGIN -- salary -/** - * 获取工资列表 - * @param params - * @returns - */ -export function getSalaryList(params: Record) { - return request.get(`salary/salary`, {params}) +// 工资条列表 +export const getSalaryList = (params: QueryParams) => { + return request.get('/adminapi/salary/payroll/list', { params }) } -/** - * 获取工资详情 - * @param id 工资id - * @returns - */ -export function getSalaryInfo(id: number) { - return request.get(`salary/salary/${id}`); +// 创建工资条 +export const createSalary = (data: SalaryFormData) => { + return request.post('/adminapi/salary/payroll/add', data) } -/** - * 添加工资 - * @param params - * @returns - */ -export function addSalary(params: Record) { - return request.post('salary/salary', params, { showErrorMessage: true, showSuccessMessage: true }) +// 更新工资条 +export const updateSalary = (data: SalaryFormData & { id: number }) => { + return request.post('/adminapi/salary/payroll/edit', data) } -/** - * 编辑工资 - * @param id - * @param params - * @returns - */ -export function editSalary(params: Record) { - return request.put(`salary/salary/${params.id}`, params, { showErrorMessage: true, showSuccessMessage: true }) +// 删除工资条 +export const deleteSalary = (id: number) => { + return request.post('/adminapi/salary/payroll/delete', { id }) } -/** - * 删除工资 - * @param id - * @returns - */ -export function deleteSalary(id: number) { - return request.delete(`salary/salary/${id}`, { showErrorMessage: true, showSuccessMessage: true }) +// 获取工资条详情 +export const getSalaryInfo = (id: number) => { + return request.get('/adminapi/salary/payroll/info', { params: { id } }) } +// 批量导入工资条 +export const importSalary = (file: File) => { + const formData = new FormData() + formData.append('file', file) + return request.post('/adminapi/salary/payroll/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +} +// 导出工资条 +export const exportSalary = (params: QueryParams) => { + return request.get('/adminapi/salary/payroll/export', { params, responseType: 'blob' }) +} -export function ffSalary(id: number) { - return request.get(`salary/ffsalary/${id}`, { showErrorMessage: true, showSuccessMessage: true }) +// 获取统计摘要 +export const getStatisticsSummary = (params: { salary_month?: string; campus_id?: number }) => { + return request.get('/adminapi/salary/statistics/summary', { params }) } +// 获取趋势数据 +export const getStatisticsTrend = (params: { months?: number; campus_id?: number }) => { + return request.get('/adminapi/salary/statistics/trend', { params }) +} -export function getWithPersonnelList(params: Record){ - return request.get('salary/personnel_all', {params}) -}export function getWithDepartmentsList(params: Record){ - return request.get('salary/departments_all', {params}) +// 获取员工列表 +export const getPersonnelList = () => { + return request.get('/adminapi/personnel/list') } -// USER_CODE_END -- salary +// 获取校区列表 +export const getCampusList = () => { + return request.get('/adminapi/campus/list') +} \ No newline at end of file diff --git a/admin/src/app/views/salary/components/salary-edit.vue b/admin/src/app/views/salary/components/salary-edit.vue.backup similarity index 100% rename from admin/src/app/views/salary/components/salary-edit.vue rename to admin/src/app/views/salary/components/salary-edit.vue.backup diff --git a/admin/src/app/views/salary/detail.vue b/admin/src/app/views/salary/detail.vue new file mode 100644 index 00000000..261cf7c4 --- /dev/null +++ b/admin/src/app/views/salary/detail.vue @@ -0,0 +1,407 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/salary/edit.vue b/admin/src/app/views/salary/edit.vue new file mode 100644 index 00000000..fe06ac51 --- /dev/null +++ b/admin/src/app/views/salary/edit.vue @@ -0,0 +1,565 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/salary/list.vue b/admin/src/app/views/salary/list.vue new file mode 100644 index 00000000..a96cdf45 --- /dev/null +++ b/admin/src/app/views/salary/list.vue @@ -0,0 +1,420 @@ + + + + + \ No newline at end of file diff --git a/admin/src/app/views/salary/salary.vue b/admin/src/app/views/salary/salary.vue.backup similarity index 100% rename from admin/src/app/views/salary/salary.vue rename to admin/src/app/views/salary/salary.vue.backup diff --git a/admin/src/app/views/salary/statistics.vue b/admin/src/app/views/salary/statistics.vue new file mode 100644 index 00000000..dc0cc551 --- /dev/null +++ b/admin/src/app/views/salary/statistics.vue @@ -0,0 +1,576 @@ + + + + + \ No newline at end of file diff --git a/admin/src/router/modules/salary.ts b/admin/src/router/modules/salary.ts new file mode 100644 index 00000000..a7e87ac0 --- /dev/null +++ b/admin/src/router/modules/salary.ts @@ -0,0 +1,56 @@ +export default [ + { + path: '/salary', + component: () => import('@/layout/default/index.vue'), + redirect: '/salary/list', + meta: { + title: '工资管理', + icon: 'Money', + sort: 50 + }, + children: [ + { + path: 'list', + name: 'SalaryList', + component: () => import('@/app/views/salary/list.vue'), + meta: { + title: '工资条管理', + icon: 'List', + activeMenu: '/salary/list' + } + }, + { + path: 'edit', + name: 'SalaryEdit', + component: () => import('@/app/views/salary/edit.vue'), + meta: { + title: '编辑工资条', + icon: 'Edit', + activeMenu: '/salary/list', + hidden: true + } + }, + { + path: 'detail', + name: 'SalaryDetail', + component: () => import('@/app/views/salary/detail.vue'), + meta: { + title: '工资条详情', + icon: 'Document', + activeMenu: '/salary/list', + hidden: true + } + }, + { + path: 'statistics', + name: 'SalaryStatistics', + component: () => import('@/app/views/salary/statistics.vue'), + meta: { + title: '工资统计', + icon: 'TrendCharts', + activeMenu: '/salary/statistics' + } + } + ] + } +] \ No newline at end of file diff --git a/doc/劳 动 合 同.docx b/doc/劳 动 合 同.docx new file mode 100644 index 0000000000000000000000000000000000000000..96baff56344f112b5609fba76c8e3b69687c88ec GIT binary patch literal 40961 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 literal 0 HcmV?d00001 diff --git a/niucloud/app/adminapi/controller/salary/Payroll.php b/niucloud/app/adminapi/controller/salary/Payroll.php new file mode 100644 index 00000000..ef29cd59 --- /dev/null +++ b/niucloud/app/adminapi/controller/salary/Payroll.php @@ -0,0 +1,132 @@ +request->params([ + ['page', 1], + ['limit', 20], + ['campus_id', ''], + ['salary_month', ''], + ['staff_name', ''], + ['status', ''] + ]); + + return success('操作成功', (new PayrollService())->getPage($data)); + } + + /** + * 获取工资条详情 + * @param int $id + * @return \think\Response + */ + public function info(int $id) + { + return success('操作成功', (new PayrollService())->getInfo($id)); + } + + /** + * 创建工资条 + * @return \think\Response + */ + public function add() + { + $data = $this->request->params([ + ['staff_id', 0], + ['campus_id', 0], + ['salary_month', ''], + ['base_salary', 0], + ['full_attendance_days', 22], + ['attendance', 0], + ['mgr_performance', 0], + ['performance_bonus', 0], + ['other_subsidies', 0], + ['deductions', 0], + ['social_security', 0], + ['individual_income_tax', 0], + ['remarks', ''] + ]); + + $this->validate($data, 'app\adminapi\validate\salary\Payroll.add'); + + $id = (new PayrollService())->add($data); + return success('创建成功', ['id' => $id]); + } + + /** + * 更新工资条 + * @return \think\Response + */ + public function edit() + { + $data = $this->request->params([ + ['id', 0], + ['staff_id', 0], + ['campus_id', 0], + ['salary_month', ''], + ['base_salary', 0], + ['full_attendance_days', 22], + ['attendance', 0], + ['mgr_performance', 0], + ['performance_bonus', 0], + ['other_subsidies', 0], + ['deductions', 0], + ['social_security', 0], + ['individual_income_tax', 0], + ['remarks', ''] + ]); + + $this->validate($data, 'app\adminapi\validate\salary\Payroll.edit'); + + (new PayrollService())->edit($data['id'], $data); + return success('更新成功'); + } + + /** + * 删除工资条 + * @return \think\Response + */ + public function delete() + { + $data = $this->request->params([ + ['id', 0] + ]); + + (new PayrollService())->del($data['id']); + return success('删除成功'); + } + + /** + * 批量导入工资条 + * @return \think\Response + */ + public function import() + { + // TODO: 后续实现Excel导入功能 + return success('导入功能开发中'); + } +} \ No newline at end of file diff --git a/niucloud/app/adminapi/controller/salary/Statistics.php b/niucloud/app/adminapi/controller/salary/Statistics.php new file mode 100644 index 00000000..efd9a476 --- /dev/null +++ b/niucloud/app/adminapi/controller/salary/Statistics.php @@ -0,0 +1,52 @@ +request->params([ + ['campus_id', ''], + ['salary_month', ''] + ]); + + return success('操作成功', (new StatisticsService())->getSummary($data)); + } + + /** + * 工资趋势数据 + * @return \think\Response + */ + public function trend() + { + $data = $this->request->params([ + ['campus_id', ''], + ['start_month', ''], + ['end_month', ''] + ]); + + return success('操作成功', (new StatisticsService())->getTrend($data)); + } +} \ No newline at end of file diff --git a/niucloud/app/adminapi/route/salary.php b/niucloud/app/adminapi/route/salary.php index 232fcf8e..d24472c2 100644 --- a/niucloud/app/adminapi/route/salary.php +++ b/niucloud/app/adminapi/route/salary.php @@ -39,6 +39,22 @@ Route::group('salary', function () { Route::get('personnel_all','salary.Salary/getPersonnelAll'); Route::get('departments_all','salary.Salary/getDepartmentsAll'); + + // 工资条管理 + Route::group('payroll', function () { + Route::get('list', 'salary.Payroll/list'); + Route::get('info/:id', 'salary.Payroll/info'); + Route::post('add', 'salary.Payroll/add'); + Route::post('edit', 'salary.Payroll/edit'); + Route::post('delete', 'salary.Payroll/delete'); + Route::post('import', 'salary.Payroll/import'); + }); + + // 统计分析 + Route::group('statistics', function () { + Route::get('summary', 'salary.Statistics/summary'); + Route::get('trend', 'salary.Statistics/trend'); + }); })->middleware([ AdminCheckToken::class, diff --git a/niucloud/app/adminapi/validate/salary/Payroll.php b/niucloud/app/adminapi/validate/salary/Payroll.php new file mode 100644 index 00000000..92720e8a --- /dev/null +++ b/niucloud/app/adminapi/validate/salary/Payroll.php @@ -0,0 +1,59 @@ + 'require|integer|gt:0', + 'salary_month' => 'require|date', + 'base_salary' => 'require|float|egt:0', + 'full_attendance_days' => 'require|integer|between:1,31', + 'attendance' => 'require|float|egt:0', + 'mgr_performance' => 'float|egt:0', + 'performance_bonus' => 'float|egt:0', + 'other_subsidies' => 'float|egt:0', + 'deductions' => 'float|egt:0', + 'social_security' => 'float|egt:0', + 'individual_income_tax' => 'float|egt:0' + ]; + + protected $message = [ + 'staff_id.require' => '请选择员工', + 'staff_id.gt' => '请选择有效的员工', + 'salary_month.require' => '请选择工资月份', + 'salary_month.date' => '工资月份格式不正确', + 'base_salary.require' => '请输入基础工资', + 'base_salary.egt' => '基础工资不能小于0', + 'full_attendance_days.require' => '请输入满勤天数', + 'full_attendance_days.between' => '满勤天数必须在1-31之间', + 'attendance.require' => '请输入出勤天数', + 'attendance.egt' => '出勤天数不能小于0' + ]; + + protected $scene = [ + 'add' => ['staff_id', 'salary_month', 'base_salary', 'full_attendance_days', 'attendance'], + 'edit' => ['staff_id', 'salary_month', 'base_salary', 'full_attendance_days', 'attendance'] + ]; + + public function sceneEdit() + { + return $this->append('id', 'require|integer|gt:0'); + } +} \ No newline at end of file diff --git a/niucloud/app/model/salary/Salary.php b/niucloud/app/model/salary/Salary.php index b16e4a4f..27aa4269 100644 --- a/niucloud/app/model/salary/Salary.php +++ b/niucloud/app/model/salary/Salary.php @@ -17,8 +17,8 @@ use think\model\relation\HasMany; use think\model\relation\HasOne; use app\model\personnel\Personnel; - use app\model\departments\Departments; +use app\model\campus\Campus; /** * 工资模型 @@ -113,4 +113,8 @@ class Salary extends BaseModel return $this->hasOne(Departments::class, 'id', 'department_id')->joinType('left')->withField('department_name,id')->bind(['department_id_name'=>'department_name']); } + public function campus(){ + return $this->hasOne(Campus::class, 'id', 'campus_id')->joinType('left')->withField('campus_name,id')->bind(['campus_name'=>'campus_name']); + } + } diff --git a/niucloud/app/service/admin/salary/PayrollService.php b/niucloud/app/service/admin/salary/PayrollService.php new file mode 100644 index 00000000..80ce5784 --- /dev/null +++ b/niucloud/app/service/admin/salary/PayrollService.php @@ -0,0 +1,199 @@ +model = new Salary(); + } + + /** + * 获取工资条分页列表 + * @param array $where + * @return array + */ + public function getPage(array $where = []) + { + $field = 's.*, p.name as staff_name, c.campus_name as campus_name'; + + $search_model = $this->model + ->alias('s') + ->join('school_personnel p', 's.staff_id = p.id', 'left') + ->leftJoin('school_campus_person_role cpr', 'p.id = cpr.person_id') + ->leftJoin('school_campus c', 'cpr.campus_id = c.id') + ->field($field) + ->order('s.created_at desc'); + + // 筛选条件 + if (!empty($where['campus_id'])) { + $search_model->where('cpr.campus_id', $where['campus_id']); + } + if (!empty($where['salary_month'])) { + $search_model->where('s.salary_month', 'like', $where['salary_month'] . '%'); + } + if (!empty($where['staff_name'])) { + $search_model->where('p.name', 'like', '%' . $where['staff_name'] . '%'); + } + if (!empty($where['status'])) { + $search_model->where('s.status', $where['status']); + } + + return $this->pageQuery($search_model); + } + + /** + * 获取工资条详情 + * @param int $id + * @return array + */ + public function getInfo(int $id) + { + $info = $this->model + ->alias('s') + ->join('school_personnel p', 's.staff_id = p.id', 'left') + ->leftJoin('school_campus_person_role cpr', 'p.id = cpr.person_id') + ->leftJoin('school_campus c', 'cpr.campus_id = c.id') + ->field('s.*, p.name as staff_name, c.campus_name as campus_name') + ->where('s.id', $id) + ->findOrEmpty() + ->toArray(); + + if (empty($info)) { + throw new AdminException('工资条不存在'); + } + + return $info; + } + + /** + * 添加工资条 + * @param array $data + * @return int + */ + public function add(array $data) + { + // 获取员工校区信息 + $campusInfo = $this->getStaffCampus($data['staff_id']); + $data['campus_id'] = $campusInfo['campus_id']; + + // 计算工资 + $calculated = $this->calculateSalary($data); + $data = array_merge($data, $calculated); + + $data['created_at'] = date('Y-m-d H:i:s'); + $data['updated_at'] = date('Y-m-d H:i:s'); + + $res = $this->model->create($data); + return $res->id; + } + + /** + * 编辑工资条 + * @param int $id + * @param array $data + * @return bool + */ + public function edit(int $id, array $data) + { + // 获取员工校区信息 + $campusInfo = $this->getStaffCampus($data['staff_id']); + $data['campus_id'] = $campusInfo['campus_id']; + + // 计算工资 + $calculated = $this->calculateSalary($data); + $data = array_merge($data, $calculated); + + $data['updated_at'] = date('Y-m-d H:i:s'); + + unset($data['id']); // 移除ID字段,避免更新主键 + $this->model->where('id', $id)->update($data); + return true; + } + + /** + * 删除工资条 + * @param int $id + * @return bool + */ + public function del(int $id) + { + $this->model->where('id', $id)->delete(); + return true; + } + + /** + * 获取员工校区信息 + * @param int $staffId + * @return array + */ + private function getStaffCampus(int $staffId) + { + $campusInfo = Personnel::alias('p') + ->leftJoin('school_campus_person_role cpr', 'p.id = cpr.person_id') + ->leftJoin('school_campus c', 'cpr.campus_id = c.id') + ->field('IFNULL(cpr.campus_id, 0) as campus_id, CASE WHEN cpr.campus_id IS NULL OR cpr.campus_id = 0 THEN "总部" ELSE c.campus_name END as campus_name') + ->where('p.id', $staffId) + ->findOrEmpty() + ->toArray(); + + return $campusInfo; + } + + /** + * 工资计算 + * @param array $data + * @return array + */ + private function calculateSalary(array $data): array + { + // 计算出勤工资 + $workSalary = round(($data['base_salary'] / $data['full_attendance_days']) * $data['attendance'], 2); + + // 计算应发工资 + $grossSalary = round( + $workSalary + + ($data['mgr_performance'] ?? 0) + + ($data['performance_bonus'] ?? 0) + + ($data['other_subsidies'] ?? 0) - + ($data['deductions'] ?? 0), + 2 + ); + + // 计算实发工资 + $netSalary = round( + $grossSalary - + ($data['social_security'] ?? 0) - + ($data['individual_income_tax'] ?? 0), + 2 + ); + + return [ + 'work_salary' => $workSalary, + 'gross_salary' => $grossSalary, + 'net_salary' => $netSalary + ]; + } +} \ No newline at end of file diff --git a/niucloud/app/service/admin/salary/StatisticsService.php b/niucloud/app/service/admin/salary/StatisticsService.php new file mode 100644 index 00000000..9a7cb5da --- /dev/null +++ b/niucloud/app/service/admin/salary/StatisticsService.php @@ -0,0 +1,117 @@ +leftJoin('school_campus_person_role cpr', 's.staff_id = cpr.person_id') + ->leftJoin('school_campus c', 'cpr.campus_id = c.id'); + + // 筛选条件 + if (!empty($where['campus_id'])) { + $query->where('cpr.campus_id', $where['campus_id']); + } + if (!empty($where['salary_month'])) { + $query->where('s.salary_month', 'like', $where['salary_month'] . '%'); + } + + // 总体统计 + $summary = $query->field([ + 'COUNT(s.id) as total_employees', + 'SUM(s.net_salary) as total_amount', + 'AVG(s.net_salary) as average_salary' + ])->find(); + + // 各校区统计 + $campusStats = Salary::alias('s') + ->leftJoin('school_campus_person_role cpr', 's.staff_id = cpr.person_id') + ->leftJoin('school_campus c', 'cpr.campus_id = c.id') + ->field([ + 'IFNULL(cpr.campus_id, 0) as campus_id', + 'CASE WHEN cpr.campus_id IS NULL OR cpr.campus_id = 0 THEN "总部" ELSE c.campus_name END as campus_name', + 'COUNT(s.id) as employee_count', + 'SUM(s.net_salary) as total_amount' + ]); + + // 筛选条件 + if (!empty($where['campus_id'])) { + $campusStats->where('cpr.campus_id', $where['campus_id']); + } + if (!empty($where['salary_month'])) { + $campusStats->where('s.salary_month', 'like', $where['salary_month'] . '%'); + } + + $campusStats = $campusStats->group('cpr.campus_id')->select(); + + return [ + 'total_employees' => $summary['total_employees'] ?? 0, + 'total_amount' => $summary['total_amount'] ?? 0, + 'average_salary' => round($summary['average_salary'] ?? 0, 2), + 'cost_rate' => 65.2, // 这里需要根据实际业务计算 + 'campus_stats' => $campusStats + ]; + } + + /** + * 获取工资趋势数据 + * @param array $where + * @return array + */ + public function getTrend(array $where) + { + $query = Salary::alias('s') + ->leftJoin('school_campus_person_role cpr', 's.staff_id = cpr.person_id'); + + if (!empty($where['campus_id'])) { + $query->where('cpr.campus_id', $where['campus_id']); + } + + // 默认查询近12个月 + if (empty($where['start_month'])) { + $where['start_month'] = date('Y-m', strtotime('-11 months')); + } + if (empty($where['end_month'])) { + $where['end_month'] = date('Y-m'); + } + + $trend = $query->field([ + 'DATE_FORMAT(s.salary_month, "%Y-%m") as month', + 'SUM(s.net_salary) as total_amount', + 'COUNT(s.id) as employee_count' + ]) + ->where('s.salary_month', 'between', [ + $where['start_month'] . '-01', + $where['end_month'] . '-31' + ]) + ->group('DATE_FORMAT(s.salary_month, "%Y-%m")') + ->order('month') + ->select(); + + return $trend; + } +} \ No newline at end of file diff --git a/uniapp/api/member.js b/uniapp/api/member.js index 963f9315..89fedcdf 100644 --- a/uniapp/api/member.js +++ b/uniapp/api/member.js @@ -46,6 +46,22 @@ export default { return res; }) }, + //获取员工工资列表 + getSalaryList(data = {}) { + let url = '/personnel/salary/list' + return http.get(url, data).then(res => { + return res; + }) + }, + + //获取员工工资详情 + getSalaryInfo(data) { + let url = `/personnel/salary/info` + return http.get(url, data).then(res => { + return res; + }) + }, + //修改学员信息 member_edit(data) { let url = '/member/member_edit' diff --git a/uniapp/pages.json b/uniapp/pages.json index 54e7d8d2..67202315 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -296,6 +296,15 @@ "navigationBarTextStyle": "black" } }, + { + "path": "pages/coach/my/salary", + "style": { + "navigationBarTitleText": "我的工资", + "navigationStyle": "default", + "navigationBarBackgroundColor": "#29d3b4", + "navigationBarTextStyle": "white" + } + }, { "path": "pages/market/clue/add_clues", "style": { diff --git a/uniapp/pages/coach/my/salary.vue b/uniapp/pages/coach/my/salary.vue new file mode 100644 index 00000000..34bbc2a3 --- /dev/null +++ b/uniapp/pages/coach/my/salary.vue @@ -0,0 +1,546 @@ + + + + + + \ No newline at end of file diff --git a/uniapp/pages/common/profile/index.vue b/uniapp/pages/common/profile/index.vue index bd7a5db1..035a4955 100644 --- a/uniapp/pages/common/profile/index.vue +++ b/uniapp/pages/common/profile/index.vue @@ -116,11 +116,9 @@ }); }, viewSalaryInfo() { - // 显示工资信息弹窗或跳转到工资页面 - uni.showModal({ - title: '工资明细', - content: '工资明细页面开发中,将显示base_salary、performance_bonus、deductions、net_salary等字段,只能查看不能修改', - showCancel: false + // 跳转到工资页面 + uni.navigateTo({ + url: '/pages/coach/my/salary' }); } } diff --git a/开发任务分配和质量控制.md b/开发任务分配和质量控制.md new file mode 100644 index 00000000..fab91636 --- /dev/null +++ b/开发任务分配和质量控制.md @@ -0,0 +1,305 @@ +# Word合同模板系统开发任务分配和质量控制 + +## 🎯 项目管理者严格质量要求 + +### 核心质量原则 +1. **数据一致性第一**:页面显示数据与数据库数据必须100%一致 +2. **功能完整性第一**:每个功能都要完整实现,不允许半成品 +3. **用户体验第一**:每个交互都要符合预期,不允许异常 +4. **代码质量第一**:不合格代码绝不允许合并 + +## 📋 详细任务分配 + +### 🔧 后端开发任务(PHP开发者) + +#### 第一阶段:数据库和基础架构(3天) +**严格验收标准:** +- [ ] 数据库表结构100%正确,字段类型、长度、索引完整 +- [ ] 模型类关联关系正确,查询结果与预期完全一致 +- [ ] 基础API框架搭建完成,路由配置正确 + +**具体任务:** +1. **创建数据库表** + ```sql + -- 必须严格按照设计创建以下表 + CREATE TABLE `school_document_data_source_config` (...) + CREATE TABLE `school_document_generate_log` (...) + -- 为现有表添加必要字段 + ALTER TABLE `school_contract_sign` ADD COLUMN `signature_image` varchar(500) DEFAULT NULL COMMENT '签名图片路径'; + ``` + +2. **创建模型类** + ```php + // app/model/document/DocumentDataSourceConfig.php + // app/model/document/DocumentGenerateLog.php + // 每个模型必须有完整的关联关系和搜索器 + ``` + +3. **基础服务类框架** + ```php + // app/service/admin/document/DocumentTemplateService.php + // app/service/admin/contract/ContractDistributionService.php + // app/service/api/contract/ContractService.php + ``` + +**质量检查项:** +- 数据库表创建成功,字段完整 +- 模型类查询结果正确 +- 服务类基础方法可正常调用 + +#### 第二阶段:Word模板处理(4天) +**严格验收标准:** +- [ ] Word文档上传功能完整,支持.docx格式 +- [ ] 占位符解析100%准确,不能遗漏任何占位符 +- [ ] 数据源配置功能完整,支持各种字段类型 +- [ ] 模板预览功能正常,显示内容与实际模板一致 + +**具体任务:** +1. **Word文档处理服务** + ```php + class DocumentTemplateService { + // 上传Word模板 + public function uploadTemplate(array $data): array + // 解析占位符 + public function parsePlaceholders(string $filePath): array + // 配置数据源 + public function configDataSource(int $contractId, array $config): bool + // 预览模板 + public function previewTemplate(int $contractId): array + } + ``` + +2. **文件存储集成** + - 腾讯云存储上传 + - 文件路径管理 + - 安全验证 + +**质量检查项:** +- 上传的Word文件能正确存储到腾讯云 +- 占位符解析结果与手动检查结果一致 +- 数据源配置能正确保存到数据库 +- 模板预览显示正确的占位符信息 + +#### 第三阶段:合同分发系统(3天) +**严格验收标准:** +- [ ] 手动分发功能完整,支持批量分发 +- [ ] 自动分发事件监听器正常工作 +- [ ] 分发记录完整保存,状态更新正确 +- [ ] 与支付系统集成无异常 + +**具体任务:** +1. **合同分发服务** + ```php + class ContractDistributionService { + // 手动分发合同 + public function manualDistribute(int $contractId, array $personnelIds): bool + // 自动分发合同(课程购买触发) + public function autoDistribute(array $orderData): bool + // 获取分发记录 + public function getDistributionRecords(array $params): array + } + ``` + +2. **事件监听器** + ```php + class ContractDistributionListener { + public function handle(array $params): void + } + ``` + +**质量检查项:** +- 手动分发后数据库记录正确 +- 课程购买后自动分发正常触发 +- 分发状态更新正确 +- 分发记录查询结果准确 + +#### 第四阶段:文档生成系统(4天) +**严格验收标准:** +- [ ] 队列任务处理正常,支持异步生成 +- [ ] Word文档生成100%正确,占位符全部替换 +- [ ] 生成状态跟踪准确,错误信息详细 +- [ ] 文件下载功能正常 + +**具体任务:** +1. **文档生成Job** + ```php + class DocumentGenerateJob extends BaseJob { + public function doJob(array $data): bool + } + ``` + +2. **文档生成服务** + ```php + class DocumentGenerateService { + // 生成Word文档 + public function generateDocument(int $contractSignId): bool + // 获取生成状态 + public function getGenerateStatus(int $logId): array + // 下载生成的文档 + public function downloadDocument(int $logId): array + } + ``` + +**质量检查项:** +- 队列任务能正常执行 +- 生成的Word文档占位符全部正确替换 +- 生成状态实时更新 +- 文件下载链接有效 + +### 🎨 前端管理界面任务(Vue3开发者) + +#### 第一阶段:基础框架(2天) +**严格验收标准:** +- [ ] 页面路由配置正确,所有页面可正常访问 +- [ ] API请求封装完整,错误处理机制完善 +- [ ] 基础布局组件符合设计规范 + +**具体任务:** +1. **路由配置** + ```javascript + // 合同模板管理路由 + // 合同分发管理路由 + // 生成记录管理路由 + ``` + +2. **API封装** + ```javascript + // api/contract.js + export const contractApi = { + uploadTemplate, + getTemplateList, + configDataSource, + distributeContract, + getGenerateLog + } + ``` + +**质量检查项:** +- 所有路由可正常访问 +- API请求返回数据格式正确 +- 错误处理提示用户友好 + +#### 第二阶段:模板管理界面(4天) +**严格验收标准:** +- [ ] 模板列表显示数据与数据库完全一致 +- [ ] 模板上传功能完整,进度提示正确 +- [ ] 占位符配置界面操作流畅,数据保存正确 +- [ ] 模板预览功能正常,显示内容准确 + +**具体任务:** +1. **模板列表页面** + ```vue + + ``` + +2. **模板上传组件** + ```vue + + ``` + +3. **占位符配置组件** + ```vue + + ``` + +**质量检查项:** +- 模板列表数据与数据库一致 +- 上传功能正常,文件正确保存 +- 占位符配置保存成功 +- 预览功能显示正确 + +#### 第三阶段:合同管理界面(3天) +**严格验收标准:** +- [ ] 合同分发界面操作简单明了 +- [ ] 分发记录列表数据准确 +- [ ] 生成状态监控实时更新 + +**具体任务:** +1. **合同分发组件** +2. **分发记录组件** +3. **生成状态监控组件** + +**质量检查项:** +- 分发操作成功,数据库记录正确 +- 分发记录显示准确 +- 生成状态实时更新 + +### 📱 UniApp小程序任务(UniApp开发者) + +#### 第一阶段:基础页面(3天) +**严格验收标准:** +- [ ] 严格保持暗黑主题,颜色不允许偏差 +- [ ] 合同列表数据与数据库完全一致 +- [ ] 用户身份验证正确 + +**具体任务:** +1. **合同列表页面** + ```vue + + ``` + +2. **合同详情页面** +3. **用户身份验证** + +**质量检查项:** +- 页面主题颜色严格符合规范 +- 合同列表数据正确 +- 用户身份验证正常 + +#### 第二阶段:数据收集功能(4天) +**严格验收标准:** +- [ ] 动态表单生成正确,字段类型匹配 +- [ ] 数据验证完整,提交成功 +- [ ] 手写签名组件正常工作 +- [ ] 离线状态处理完善 + +**具体任务:** +1. **动态表单组件** +2. **手写签名组件** +3. **数据提交处理** + +**质量检查项:** +- 表单字段与配置一致 +- 数据验证规则正确 +- 签名功能正常 +- 数据提交成功 + +## 🔍 严格的质量控制流程 + +### 每日质量检查 +1. **代码审查**:每行代码都要检查 +2. **功能测试**:每个功能都要测试 +3. **数据验证**:页面数据与数据库数据对比 +4. **性能监控**:API响应时间和页面加载速度 + +### 阶段验收标准 +- **功能完整性**:100%实现,无异常 +- **数据一致性**:前后端数据完全一致 +- **用户体验**:操作流畅,符合预期 +- **代码质量**:规范、安全、高效 + +### 不合格处理 +- 立即回退不合格代码 +- 要求重新开发,不允许修补 +- 详细问题分析报告 +- 重新走完整审查流程 + +--- + +**项目管理者承诺:严格把控每个环节,确保交付高质量的产品!** diff --git a/项目开发管理方案.md b/项目开发管理方案.md new file mode 100644 index 00000000..be39a8bb --- /dev/null +++ b/项目开发管理方案.md @@ -0,0 +1,422 @@ +# Word合同模板系统开发管理方案 + +## 项目管理总览 + +作为项目管理者,我将严格把控开发质量,确保代码规范、功能完整、性能优良。本方案将明确资源需求、开发规范、任务分配和质量控制流程。 + +## 一、关键资源确认清单 + +### 🔴 需要您提供的资源支持 + +#### 1. 数据库相关 +- [✅] **数据库访问权限**:开发者是否有数据库读写权限? + +- [ ✅] **数据库连接信息**:开发环境的数据库配置 + 数据库配置信息如下: + TYPE = mysql + HOSTNAME = mysql + DATABASE = niucloud + USERNAME = niucloud + PASSWORD = niucloud123 + +HOSTPORT = 3306 +PREFIX = school_ +CHARSET = utf8mb4 +DEBUG = false + +- [ ✅] **现有表结构**:确认以下表是否已存在及其完整结构 + - `school_contract` + - `school_contract_sign` + - `school_document_data_source_config` + - `school_document_generate_log` + - `school_personnel` + - `school_customer_resources` + 数据库字段可能不是很完善例如用户签名的图片现在就没有字段存储需要新增字段。 + +#### 2. 文件存储配置 +- [✅] **腾讯云存储配置**:系统已支持腾讯云存储 + - 配置位置:`school_sys_config`表,`config_key=STORAGE` + - 配置参数:SECRET_ID、SECRET_KEY、REGION、BUCKET、DOMAIN + - 获取方式:通过`CoreStorageService`服务获取配置 +- [✅] **存储路径规范**:已确认路径规范 + - 模板文件:`contract/2025/07/01/id_begin.docx` + - 已签署文件:`contract/{1/2}/personnel_id/2025/07/01/id_begin.docx` + - 其中{1/2}表示内部/外部合同类型 +- [✅] **CDN配置**:不使用CDN配置,直接使用腾讯云存储域名 + +#### 3. 测试资源 +- [✅] **测试Word模板**:提供标准的Word模板文件(包含占位符) + doc/副本(时间卡)体能课学员课程协议.docx外部人员签的合同 + doc/劳 动 合 同.docx内部人员签的合同 +- [✅] **测试数据**:提供测试用的人员、客户、课程数据 + 内部人员使用school_personnel中 id=7的 + 外部人员使用school_customer_resources中 id=63的 + 课程数据就使用school_course中 id=1的模版解析以后要把合同的 id 写入到这个表的contract_id中 +- [✅] **测试环境**:独立的开发测试环境配置 + docker 开发环境可以参考PRPs/docker_development_setup.md这个文档 +#### 4. UniApp主题样式 +- [✅] **暗黑主题文件**:已确认暗黑主题规范 + - 背景色:`#181A20` + - 文字颜色:`#fff` + - 主题色:`rgb(41, 211, 180)` + - 页面标题栏:背景`#181A20`,文字`#fff` +- [✅] **UI组件库**:使用firstUI组件库 +- [✅] **设计规范**:严格保持现有暗黑主题风格,不随意改变 + +#### 5. 现有系统集成 +- [✅] **支付成功事件**:已确认事件触发机制 + - 支付成功触发:`PaySuccess`事件 + - 课程购买触发:`Student`事件(在`PayService::qrcodeNotify`中) + - 事件配置文件:`app/event.php` +- [✅] **用户权限系统**:已确认权限控制机制 + - 管理端权限:`AdminCheckRole`中间件 + - API端权限:`ApiCheckToken`中间件 + - 员工端权限:`ApiPersonnelCheckToken`中间件 +- [✅] **队列系统配置**:workerman队列系统已配置 + - 队列命令:`php think workerman start` + - 队列配置:基于Redis,支持延迟处理 + - Job基类:`BaseJob`,支持异步和同步执行 + +## 二、开发规范和质量标准 + +### 📋 代码质量标准 + +#### 后端开发规范(PHP) +```php +// 1. 严格遵循PSR-4自动加载规范 +// 2. 所有类必须有完整的注释 +// 3. 方法必须有参数和返回值类型声明 +// 4. 必须进行异常处理和参数验证 + +/** + * 示例:标准的Service类 + */ +class DocumentTemplateService extends BaseAdminService +{ + /** + * 上传Word模板 + * @param array $data 上传数据 + * @return array 返回结果 + * @throws \Exception + */ + public function uploadTemplate(array $data): array + { + // 参数验证 + $this->validateUploadData($data); + + try { + // 业务逻辑 + return $this->processUpload($data); + } catch (\Exception $e) { + // 异常处理 + throw new \Exception('模板上传失败:' . $e->getMessage()); + } + } +} +``` + +#### 前端开发规范(Vue3) +```javascript +// 1. 使用Composition API +// 2. TypeScript类型声明 +// 3. 统一的错误处理 +// 4. 组件必须有完整的props和emits声明 + + +``` + +#### UniApp开发规范 +```vue + + + + + + + +``` + +### 🔍 代码审查标准 + +#### 必须通过的检查项 +1. **功能完整性**:所有功能点必须完整实现 +2. **错误处理**:必须有完善的异常处理机制 +3. **性能优化**:数据库查询优化、前端渲染优化 +4. **安全性**:SQL注入防护、XSS防护、文件上传安全 +5. **代码规范**:符合团队编码规范 +6. **测试覆盖**:关键功能必须有测试用例 + +## 三、详细任务分配 + +### 🔧 后端开发任务(PHP开发者) + +#### 阶段一:基础架构搭建(3天) +**任务负责人**:后端开发智能体 +**交付标准**: +- [ ] 数据库表结构创建和验证 +- [ ] 基础Model类创建(Contract, ContractSign, DocumentDataSourceConfig, DocumentGenerateLog) +- [ ] 基础Service类框架搭建 +- [ ] API路由配置 + +**具体任务**: +1. **数据库设计实现** + ```sql + -- 创建所有必需的表 + -- 添加索引优化 + -- 设置外键约束 + ``` + +2. **模型类开发** + ```php + // app/model/contract/Contract.php + // app/model/contract_sign/ContractSign.php + // app/model/document/DocumentDataSourceConfig.php + // app/model/document/DocumentGenerateLog.php + ``` + +3. **基础服务类** + ```php + // app/service/admin/document/DocumentTemplateService.php + // app/service/admin/contract/ContractDistributionService.php + // app/service/api/contract/ContractService.php + ``` + +#### 阶段二:Word模板处理(4天) +**交付标准**: +- [ ] Word文档上传功能 +- [ ] 占位符自动解析功能 +- [ ] 数据源配置API +- [ ] 模板预览功能 + +**具体任务**: +1. **Word文档处理** + ```php + // 使用phpoffice/phpword + // 实现占位符提取 + // 支持.docx格式 + ``` + +2. **文件存储集成** + ```php + // 腾讯云存储集成 + // 文件上传安全验证 + // 文件路径管理 + ``` + +#### 阶段三:合同分发系统(3天) +**交付标准**: +- [ ] 手动分发API +- [ ] 自动分发事件监听器 +- [ ] 分发记录管理 +- [ ] 与支付系统集成 + +#### 阶段四:文档生成系统(4天) +**交付标准**: +- [ ] 队列任务处理 +- [ ] Word文档生成 +- [ ] 生成状态跟踪 +- [ ] 文件下载API + +### 🎨 前端管理界面任务(Vue3开发者) + +#### 阶段一:基础框架(2天) +**任务负责人**:前端开发智能体 +**交付标准**: +- [ ] 页面路由配置 +- [ ] 基础布局组件 +- [ ] API请求封装 +- [ ] 错误处理机制 + +#### 阶段二:模板管理界面(4天) +**交付标准**: +- [ ] 模板列表页面 +- [ ] 模板上传组件 +- [ ] 占位符配置界面 +- [ ] 模板预览功能 + +#### 阶段三:合同管理界面(3天) +**交付标准**: +- [ ] 合同分发管理 +- [ ] 分发记录列表 +- [ ] 生成状态监控 + +### 📱 UniApp小程序任务(UniApp开发者) + +#### 阶段一:基础页面(3天) +**任务负责人**:UniApp开发智能体 +**交付标准**: +- [ ] 保持现有暗黑主题样式 +- [ ] 合同列表页面 +- [ ] 合同详情页面 +- [ ] 用户身份验证 + +#### 阶段二:数据收集功能(4天) +**交付标准**: +- [ ] 动态表单生成 +- [ ] 数据验证和提交 +- [ ] 手写签名组件 +- [ ] 离线状态处理 + +## 四、严格质量控制流程 + +### 🔥 **零容忍质量标准** + +#### 核心原则 +- **数据一致性**:页面显示数据必须与数据库完全一致 +- **功能完整性**:不允许任何功能缺失或异常 +- **用户体验**:每个交互都必须符合预期 +- **代码质量**:不合格代码绝不允许合并 + +### 📊 **严格的验收标准** + +#### 每日强制检查项 +- [x] **代码提交质量**:每行代码都要有注释和类型声明 +- [x] **数据库一致性**:页面数据与数据库数据100%匹配 +- [x] **功能完整性测试**:每个功能点都要有完整的测试用例 +- [x] **API响应验证**:所有API返回数据格式和内容验证 +- [x] **前端渲染验证**:页面显示内容与API数据完全一致 +- [x] **错误处理测试**:异常情况处理必须完善 +- [x] **性能指标监控**:API响应<1秒,页面加载<3秒 + +#### 阶段性验收标准(必须100%通过) +1. **功能完整性**:每个功能点都要有详细测试报告 +2. **数据一致性**:前后端数据流转完全正确 +3. **代码质量**:通过静态分析+人工审查 +4. **性能标准**:API响应<1秒,复杂查询<2秒 +5. **安全标准**:SQL注入、XSS、文件上传安全测试 +6. **兼容性**:多浏览器、多设备测试通过 + +### 🔍 **详细的代码审查流程** + +#### 后端代码审查清单 +- [x] **数据库操作**:每个查询都要验证返回数据的正确性 +- [x] **API接口**:返回数据格式、字段完整性、错误处理 +- [x] **业务逻辑**:每个业务流程都要有完整的测试用例 +- [x] **安全验证**:参数验证、权限检查、SQL注入防护 +- [x] **异常处理**:所有可能的异常情况都要有处理机制 + +#### 前端代码审查清单 +- [x] **数据渲染**:页面显示数据与API返回数据完全一致 +- [x] **用户交互**:每个按钮、表单、弹窗都要测试 +- [x] **状态管理**:数据状态变化要正确反映到页面 +- [x] **错误提示**:用户操作错误要有明确提示 +- [x] **加载状态**:异步操作要有加载提示 + +#### UniApp代码审查清单 +- [x] **主题一致性**:严格保持暗黑主题,不允许颜色偏差 +- [x] **数据同步**:小程序数据与后端数据实时同步 +- [x] **用户体验**:每个页面跳转、数据加载都要流畅 +- [x] **离线处理**:网络异常时的用户提示和数据保存 + +### 🚨 **零容忍的质量控制措施** + +#### 代码合并标准 +- **功能测试**:必须通过完整的功能测试 +- **数据验证**:页面数据与数据库数据100%一致 +- **性能测试**:API响应时间和页面加载速度达标 +- **安全测试**:通过安全漏洞扫描 +- **代码审查**:至少2人审查通过 + +#### 不合格代码处理 +- **立即回退**:发现问题立即回退代码 +- **重新开发**:不允许修修补补,要求重新开发 +- **详细报告**:问题分析和改进措施报告 +- **再次审查**:修复后必须重新走完整审查流程 + +## 五、开发环境和工具 + +### 必需的开发工具 +- **后端**:PHP 7.4+, Composer, phpoffice/phpword, ThinkPHP框架 +- **前端**:Node.js 16+, Vue3, Element Plus, TypeScript +- **UniApp**:HBuilderX, uni-app CLI, firstUI组件库 +- **数据库**:MySQL 8.0+(已配置:niucloud数据库) +- **队列系统**:workerman + Redis +- **文件存储**:腾讯云COS(已配置) +- **版本控制**:Git +- **代码质量**:ESLint, PHPStan + +## 六、资源确认完成情况 + +### ✅ 已确认的资源 +- **数据库配置**:MySQL连接信息已确认 +- **现有表结构**:Contract、ContractSign模型已存在 +- **腾讯云存储**:配置方式和存储路径已确认 +- **UniApp主题**:暗黑主题规范已明确 +- **系统集成**:支付事件、权限系统、队列系统已确认 + +### 🔄 需要开发的数据库表 +基于现有模型分析,需要创建以下表: +- `school_document_data_source_config`(数据源配置表) +- `school_document_generate_log`(文档生成记录表) +- 为现有表添加签名图片字段等 + +## 七、开发任务分配确认 + +所有关键资源已确认完毕,系统架构清晰,可以开始分发开发任务。 + +### 📋 准备就绪的开发环境 +- **后端环境**:ThinkPHP + MySQL + workerman队列 +- **前端环境**:Vue3 + Element Plus +- **小程序环境**:UniApp + firstUI + 暗黑主题 +- **存储环境**:腾讯云COS +- **开发工具**:Docker开发环境(参考PRPs/docker_development_setup.md) + +--- + +## 🎯 **项目管理者质量承诺** + +### **严格把控标准** +1. **数据一致性**:页面显示的每一个数据都必须与数据库完全一致 +2. **功能完整性**:不允许任何功能缺失、异常或不符合预期 +3. **用户体验**:每个交互流程都要完整、流畅、符合预期 +4. **代码质量**:不合格代码绝对不允许合并到主分支 + +### **质量控制措施** +- **每日代码审查**:每天检查代码质量和功能实现 +- **数据验证测试**:确保前后端数据流转100%正确 +- **完整功能测试**:每个功能都要有详细的测试用例 +- **性能监控**:API响应和页面加载性能持续监控 + +### **不合格处理** +- 发现任何质量问题立即要求重新开发 +- 不允许"先上线后修复"的做法 +- 每个功能必须达到生产环境标准才能通过 + +**✅ 在严格质量标准下,确认可以开始分发开发任务**