diff --git a/admin/src/views/finance/account_cost/edit.vue b/admin/src/views/finance/account_cost/edit.vue index 63820c76..990bc581 100644 --- a/admin/src/views/finance/account_cost/edit.vue +++ b/admin/src/views/finance/account_cost/edit.vue @@ -34,6 +34,20 @@ /> + + + () @@ -80,11 +101,17 @@ const popupRef = shallowRef>() const mode = ref<'add' | 'edit'>('add') const popupTitle = computed(() => (mode.value === 'edit' ? '编辑账户消耗' : '新增账户消耗')) +const treeProps = { + value: 'id', + label: 'name', + children: 'children', +} const formData = reactive({ id: '', cost_date: '', media_channel_code: '', + dept_id: '' as string | number, amount: 0, remark: '', }) @@ -104,6 +131,13 @@ const formRules = { trigger: ['change'], }, ], + dept_id: [ + { + required: true, + message: '请选择绑定部门', + trigger: ['change'], + }, + ], amount: [ { required: true, @@ -117,6 +151,7 @@ const resetForm = () => { formData.id = '' formData.cost_date = '' formData.media_channel_code = props.defaultMediaChannelCode || '' + formData.dept_id = '' formData.amount = 0 formData.remark = '' } @@ -142,6 +177,7 @@ const setFormData = (data: Record) => { formData.id = data.id ?? '' formData.cost_date = data.cost_date ?? '' formData.media_channel_code = data.media_channel_code ?? '' + formData.dept_id = data.dept_id ? Number(data.dept_id) : '' formData.amount = Number(data.amount ?? 0) formData.remark = data.remark ?? '' } diff --git a/admin/src/views/finance/account_cost/index.vue b/admin/src/views/finance/account_cost/index.vue index 888dc4bf..8a65f680 100644 --- a/admin/src/views/finance/account_cost/index.vue +++ b/admin/src/views/finance/account_cost/index.vue @@ -37,6 +37,20 @@ /> + + + + @@ -103,6 +118,7 @@ v-if="showEdit" ref="editRef" :media-channel-options="mediaChannelOptions" + :dept-options="deptOptions" :default-media-channel-code="defaultMediaChannelCode" @success="getLists" @close="showEdit = false" @@ -122,6 +138,12 @@ interface MediaChannelOption { name: string } +interface DeptOption { + id: number + name: string + children?: DeptOption[] +} + const editRef = shallowRef>() const showEdit = ref(false) @@ -129,6 +151,7 @@ const queryParams = reactive({ start_date: '', end_date: '', media_channel_code: '', + dept_id: '', remark: '', }) @@ -147,8 +170,19 @@ const mediaChannelOptions = computed(() => { })) }) +const deptOptions = computed(() => { + const options = pager.extend.dept_options + return Array.isArray(options) ? options : [] +}) + const defaultMediaChannelCode = computed(() => String(pager.extend.default_media_channel_code || '')) +const treeProps = { + value: 'id', + label: 'name', + children: 'children', +} + const handleAdd = async () => { showEdit.value = true await nextTick() @@ -172,6 +206,7 @@ const handleReset = () => { queryParams.start_date = '' queryParams.end_date = '' queryParams.media_channel_code = '' + queryParams.dept_id = '' queryParams.remark = '' resetPage() } diff --git a/server/app/adminapi/lists/finance/AccountCostLists.php b/server/app/adminapi/lists/finance/AccountCostLists.php index 5596379f..52b5fd1a 100644 --- a/server/app/adminapi/lists/finance/AccountCostLists.php +++ b/server/app/adminapi/lists/finance/AccountCostLists.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace app\adminapi\lists\finance; +use app\adminapi\logic\dept\DeptLogic; use app\adminapi\lists\BaseAdminDataLists; use app\common\lists\ListsExtendInterface; use app\common\lists\ListsSearchInterface; @@ -14,8 +15,13 @@ class AccountCostLists extends BaseAdminDataLists implements ListsSearchInterfac { public function setSearch(): array { + $searchFields = ['remark', 'creator_name', 'updater_name']; + if (AccountCost::supportsDeptBinding()) { + $searchFields[] = 'dept_name'; + } + return [ - '%like%' => ['remark', 'creator_name', 'updater_name'], + '%like%' => $searchFields, ]; } @@ -35,6 +41,10 @@ class AccountCostLists extends BaseAdminDataLists implements ListsSearchInterfac $query->where('media_channel_code', trim((string) $this->params['media_channel_code'])); } + if (AccountCost::supportsDeptBinding() && !empty($this->params['dept_id'])) { + $query->where('dept_id', (int) $this->params['dept_id']); + } + return $query; } @@ -58,6 +68,8 @@ class AccountCostLists extends BaseAdminDataLists implements ListsSearchInterfac 'total_amount' => round((float) $this->baseQuery()->sum('amount'), 2), 'days_count' => (int) $this->baseQuery()->distinct(true)->count('cost_date'), 'media_channel_options' => MediaChannelService::getOptions(), + 'dept_options' => DeptLogic::getAllData(), + 'supports_dept_binding' => AccountCost::supportsDeptBinding(), 'default_media_channel_code' => MediaChannelService::getDefaultCode(), ]; } diff --git a/server/app/adminapi/logic/finance/AccountCostLogic.php b/server/app/adminapi/logic/finance/AccountCostLogic.php index c0807de2..218ff915 100644 --- a/server/app/adminapi/logic/finance/AccountCostLogic.php +++ b/server/app/adminapi/logic/finance/AccountCostLogic.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace app\adminapi\logic\finance; use app\common\logic\BaseLogic; +use app\common\model\dept\Dept; use app\common\model\finance\AccountCost; use app\common\service\qywx\MediaChannelService; @@ -16,14 +17,31 @@ class AccountCostLogic extends BaseLogic $costDate = (string) $params['cost_date']; $mediaChannelCode = trim((string) ($params['media_channel_code'] ?? '')); $mediaChannelName = MediaChannelService::getNameByCode($mediaChannelCode); + $supportsDeptBinding = AccountCost::supportsDeptBinding(); + $deptId = $supportsDeptBinding ? (int) ($params['dept_id'] ?? 0) : 0; + $deptName = $supportsDeptBinding ? self::resolveDeptName($deptId) : ''; - if (AccountCost::where('cost_date', $costDate)->where('media_channel_code', $mediaChannelCode)->count() > 0) { - self::setError('该日期下所选渠道的账户消耗已存在,请直接编辑'); + if ($supportsDeptBinding && $deptName === '') { + self::setError('请选择绑定部门'); return false; } - AccountCost::create([ + $existsQuery = AccountCost::where('cost_date', $costDate) + ->where('media_channel_code', $mediaChannelCode); + if ($supportsDeptBinding) { + $existsQuery->where('dept_id', $deptId); + } + + if ($existsQuery->count() > 0) { + self::setError($supportsDeptBinding + ? '该日期下所选渠道和部门的账户消耗已存在,请直接编辑' + : '该日期下所选渠道的账户消耗已存在,请直接编辑'); + + return false; + } + + $payload = [ 'cost_date' => $costDate, 'media_channel_code' => $mediaChannelCode, 'media_channel_name' => $mediaChannelName, @@ -33,7 +51,13 @@ class AccountCostLogic extends BaseLogic 'creator_name' => $adminName, 'updater_id' => $adminId, 'updater_name' => $adminName, - ]); + ]; + if ($supportsDeptBinding) { + $payload['dept_id'] = $deptId; + $payload['dept_name'] = $deptName; + } + + AccountCost::create($payload); return true; } catch (\Throwable $e) { @@ -53,6 +77,31 @@ class AccountCostLogic extends BaseLogic return false; } + $supportsDeptBinding = AccountCost::supportsDeptBinding(); + $deptId = $supportsDeptBinding ? (int) ($params['dept_id'] ?? 0) : 0; + $deptName = $supportsDeptBinding ? self::resolveDeptName($deptId) : ''; + if ($supportsDeptBinding && $deptName === '') { + self::setError('请选择绑定部门'); + + return false; + } + + if ($supportsDeptBinding) { + if (AccountCost::where('id', '<>', (int) $params['id']) + ->where('cost_date', (string) $model->cost_date) + ->where('media_channel_code', (string) $model->media_channel_code) + ->where('dept_id', $deptId) + ->count() > 0) { + self::setError('该日期下所选渠道和部门的账户消耗已存在,请直接编辑'); + + return false; + } + } + + if ($supportsDeptBinding) { + $model->dept_id = $deptId; + $model->dept_name = $deptName; + } $model->amount = round((float) $params['amount'], 2); $model->remark = (string) ($params['remark'] ?? ''); $model->updater_id = $adminId; @@ -91,4 +140,18 @@ class AccountCostLogic extends BaseLogic return false; } } + + private static function resolveDeptName(int $deptId): string + { + if ($deptId <= 0) { + return ''; + } + + $dept = Dept::find($deptId); + if (!$dept) { + return ''; + } + + return (string) ($dept->name ?? ''); + } } diff --git a/server/app/adminapi/logic/stats/ConversionLogic.php b/server/app/adminapi/logic/stats/ConversionLogic.php index c2a14670..a78ffe8d 100644 --- a/server/app/adminapi/logic/stats/ConversionLogic.php +++ b/server/app/adminapi/logic/stats/ConversionLogic.php @@ -6,6 +6,7 @@ namespace app\adminapi\logic\stats; use app\adminapi\logic\dept\DeptLogic; use app\adminapi\logic\tcm\DiagnosisLogic; +use app\common\model\finance\AccountCost; use app\common\service\qywx\MediaChannelService; use think\facade\Db; @@ -28,10 +29,9 @@ class ConversionLogic $pageNo = max(1, (int)($params['page_no'] ?? 1)); $pageSize = max(1, min(100, (int)($params['page_size'] ?? 15))); - $allocationParams = self::buildAllocationScopeParams($dimension, $params); $entities = self::loadEntities($dimension, $params); $entityIds = array_keys($entities); - $allocationEntities = self::loadEntities($dimension, $allocationParams); + $allocationEntities = self::loadEntities($dimension, $params); $allocationEntityIds = array_keys($allocationEntities); if ($entityIds === []) { @@ -60,11 +60,54 @@ class ConversionLogic $adminToDeptIds = self::loadAdminDeptMap(); self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel); - $allocationAddFansCount = self::sumEntityAddFans(array_values($allocationEntities)); self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel); self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel); self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel); - $globalAccountCost = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode); + [$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode); + $supportsDeptBinding = AccountCost::supportsDeptBinding(); + $restrictAccountCostByDept = $supportsDeptBinding; + $restrictStatsByDept = $supportsDeptBinding && $mediaChannelCode !== ''; + $eligibleDeptIds = $restrictAccountCostByDept ? self::expandDeptIdsWithDescendants($accountCostDeptIds) : $accountCostDeptIds; + + if ($restrictStatsByDept) { + $entities = self::filterEntitiesByChannelDeptScope($entities, $dimension, $eligibleDeptIds, $adminToDeptIds); + $allocationEntities = self::filterEntitiesByChannelDeptScope($allocationEntities, $dimension, $eligibleDeptIds, $adminToDeptIds); + } + + $entityIds = array_keys($entities); + $allocationEntityIds = array_keys($allocationEntities); + + if ($entityIds === []) { + $result = [ + 'dimension' => $dimension, + 'date_range' => [$startDate, $endDate], + 'summary' => self::buildSummary([]), + 'charts' => self::buildCharts([]), + 'lists' => [], + 'count' => 0, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + 'extend' => [ + 'dimension' => $dimension, + 'date_range' => [$startDate, $endDate], + 'summary' => self::buildSummary([]), + 'charts' => self::buildCharts([]), + ], + ]; + if ($includeFilters) { + $result['extend']['filters'] = self::buildFilterOptions(); + } + + return $result; + } + + $allocationAddFansCount = self::sumEntityAddFansForAccountCost( + array_values($allocationEntities), + $dimension, + $restrictAccountCostByDept, + $eligibleDeptIds, + $adminToDeptIds + ); if ($dimension === 'dept') { [$allRows, $pagedRows, $chartRows] = self::buildDeptTreeRows( @@ -73,7 +116,10 @@ class ConversionLogic $pageNo, $pageSize, $filterEmptyEntities, - $allocationAddFansCount + $allocationAddFansCount, + $restrictAccountCostByDept, + $eligibleDeptIds, + $adminToDeptIds ); $result = [ @@ -99,7 +145,15 @@ class ConversionLogic return $result; } - $rows = self::finalizeRows($entities, $filterEmptyEntities, $allocationAddFansCount); + $rows = self::finalizeRows( + $entities, + $filterEmptyEntities, + $allocationAddFansCount, + $restrictAccountCostByDept, + $eligibleDeptIds, + $adminToDeptIds, + $dimension + ); $count = count($rows); $offset = ($pageNo - 1) * $pageSize; @@ -126,15 +180,6 @@ class ConversionLogic return $result; } - private static function buildAllocationScopeParams(string $dimension, array $params): array - { - $allocationParams = $params; - - unset($allocationParams['dept_id'], $allocationParams['assistant_id'], $allocationParams['doctor_id']); - - return $allocationParams; - } - /** * @return array */ @@ -373,6 +418,67 @@ class ConversionLogic return $map; } + /** + * @param int[] $deptIds + * @return int[] + */ + private static function expandDeptIdsWithDescendants(array $deptIds): array + { + if ($deptIds === []) { + return []; + } + + $expanded = []; + foreach (array_values(array_unique(array_filter(array_map('intval', $deptIds), static fn (int $deptId): bool => $deptId > 0))) as $deptId) { + foreach (DeptLogic::getSelfAndDescendantIds($deptId) as $expandedDeptId) { + if ($expandedDeptId > 0) { + $expanded[$expandedDeptId] = $expandedDeptId; + } + } + } + + return array_values($expanded); + } + + /** + * 自媒体渠道选中时,统计范围只允许落在渠道绑定部门及其子部门内。 + * + * @param array> $entities + * @param array $adminToDeptIds + * @return array> + */ + private static function filterEntitiesByChannelDeptScope(array $entities, string $dimension, array $eligibleDeptIds, array $adminToDeptIds): array + { + if ($eligibleDeptIds === []) { + return []; + } + + $eligibleDeptMap = array_fill_keys(array_map('intval', $eligibleDeptIds), true); + $filtered = []; + foreach ($entities as $key => $entity) { + $entityId = (int)($entity['id'] ?? 0); + if ($entityId <= 0) { + continue; + } + + if ($dimension === 'dept') { + if (isset($eligibleDeptMap[$entityId])) { + $filtered[$key] = $entity; + } + continue; + } + + foreach (($adminToDeptIds[$entityId] ?? []) as $deptId) { + if (isset($eligibleDeptMap[(int)$deptId])) { + $filtered[$key] = $entity; + break; + } + } + } + + return $filtered; + } + /** * @param array> $entities * @param int[] $entityIds @@ -639,22 +745,43 @@ class ConversionLogic } /** - * 账户消耗:来源于独立维护表 zyt_account_cost,为全局日维度数据。 - * 由于当前没有部门/医助/医生拆分来源,只在当前筛选口径的汇总层使用,不向各明细行分摊。 + * 账户消耗:来源于独立维护表 zyt_account_cost。 * * @param array> $entities + * @return array{0: float, 1: int[]} */ - private static function hydrateAccountCostStats(array &$entities, string $startDate, string $endDate, string $mediaChannelCode): float + private static function hydrateAccountCostStats(array &$entities, string $startDate, string $endDate, string $mediaChannelCode): array { + $supportsDeptBinding = AccountCost::supportsDeptBinding(); $query = Db::name('account_cost') ->where('cost_date', '>=', $startDate) ->where('cost_date', '<=', $endDate); + if ($supportsDeptBinding) { + $query->field('dept_id, amount'); + } else { + $query->field('amount'); + } + if ($mediaChannelCode !== '') { $query->where('media_channel_code', $mediaChannelCode); } - $totalAmount = round((float) $query->sum('amount'), 2); + if ($supportsDeptBinding) { + $query->where('dept_id', '>', 0); + } + + $rows = $query->select()->toArray(); + $totalAmount = 0.0; + $deptIds = []; + foreach ($rows as $row) { + $amount = round((float)($row['amount'] ?? 0), 2); + $totalAmount = round($totalAmount + $amount, 2); + $deptId = $supportsDeptBinding ? (int)($row['dept_id'] ?? 0) : 0; + if ($supportsDeptBinding && $deptId > 0) { + $deptIds[$deptId] = $deptId; + } + } foreach ($entities as &$entity) { $entity['account_cost'] = 0.0; @@ -662,7 +789,7 @@ class ConversionLogic } unset($entity); - return $totalAmount; + return [$totalAmount, array_values($deptIds)]; } @@ -694,7 +821,15 @@ class ConversionLogic * @param array> $entities * @return array> */ - private static function finalizeRows(array $entities, bool $excludeEmpty = false, ?int $allocationAddFansCount = null): array + private static function finalizeRows( + array $entities, + bool $excludeEmpty = false, + ?int $allocationAddFansCount = null, + bool $restrictAccountCostByDept = false, + array $eligibleDeptIds = [], + array $adminToDeptIds = [], + string $dimension = 'dept' + ): array { $rows = []; $filteredEntities = $excludeEmpty @@ -714,7 +849,8 @@ class ConversionLogic $completedOrderCount = (int)$entity['completed_order_count']; $businessOrderAmount = round((float)$entity['business_order_amount'], 2); $completedOrderAmount = round((float)$entity['completed_order_amount'], 2); - $effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount); + $isEligibleForAccountCost = self::isAccountCostEligible($dimension, $entity, $restrictAccountCostByDept, $eligibleDeptIds, $adminToDeptIds); + $effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount, $isEligibleForAccountCost); $entity['paid_appointment_count'] = $paidAppointmentCount; $entity['free_appointment_count'] = $freeAppointmentCount; @@ -758,7 +894,17 @@ class ConversionLogic * @param array> $entities * @return array{0: array>, 1: array>, 2: array>} */ - private static function buildDeptTreeRows(array $entities, int $selectedDeptId, int $pageNo, int $pageSize, bool $excludeEmpty = false, ?int $allocationAddFansCount = null): array + private static function buildDeptTreeRows( + array $entities, + int $selectedDeptId, + int $pageNo, + int $pageSize, + bool $excludeEmpty = false, + ?int $allocationAddFansCount = null, + bool $restrictAccountCostByDept = false, + array $eligibleDeptIds = [], + array $adminToDeptIds = [] + ): array { $childrenByPid = []; foreach ($entities as $entity) { @@ -845,11 +991,11 @@ class ConversionLogic $globalAccountCost = self::extractGlobalAccountCost($allRowsRaw); $globalAddFansCount = $allocationAddFansCount ?? self::sumNodeAddFans($allRowsRaw); $allRows = array_map( - static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount), + static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount, $restrictAccountCostByDept, $eligibleDeptIds, $adminToDeptIds), $allRowsRaw ); $chartRows = array_map( - static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount), + static fn (array $row): array => self::finalizeDeptNode($row, $globalAccountCost, $globalAddFansCount, $restrictAccountCostByDept, $eligibleDeptIds, $adminToDeptIds), $chartRowsRaw ); @@ -910,10 +1056,17 @@ class ConversionLogic * @param array $node * @return array */ - private static function finalizeDeptNode(array $node, float $globalAccountCost, int $globalAddFansCount): array + private static function finalizeDeptNode( + array $node, + float $globalAccountCost, + int $globalAddFansCount, + bool $restrictAccountCostByDept = false, + array $eligibleDeptIds = [], + array $adminToDeptIds = [] + ): array { $node['children'] = array_map( - static fn (array $child): array => self::finalizeDeptNode($child, $globalAccountCost, $globalAddFansCount), + static fn (array $child): array => self::finalizeDeptNode($child, $globalAccountCost, $globalAddFansCount, $restrictAccountCostByDept, $eligibleDeptIds, $adminToDeptIds), $node['children'] ?? [] ); $paidAppointmentCount = (int)($node['paid_appointment_count'] ?? 0); @@ -925,14 +1078,19 @@ class ConversionLogic $completedOrderCount = (int)$node['completed_order_count']; $businessOrderAmount = round((float)$node['business_order_amount'], 2); $completedOrderAmount = round((float)$node['completed_order_amount'], 2); - $effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount); + $isEligibleForAccountCost = self::isDeptAccountCostEligible((int) ($node['id'] ?? 0), $restrictAccountCostByDept, $eligibleDeptIds); + $effectiveAccountCost = self::allocateAccountCost($globalAccountCost, $globalAddFansCount, $addFansCount, $isEligibleForAccountCost); + $childrenAccountCost = 0.0; + foreach ($node['children'] as $child) { + $childrenAccountCost = round($childrenAccountCost + (float)($child['account_cost'] ?? 0), 2); + } $node['paid_appointment_count'] = $paidAppointmentCount; $node['free_appointment_count'] = $freeAppointmentCount; $node['appointment_total_count'] = $appointmentTotalCount; $node['business_order_amount'] = $businessOrderAmount; $node['completed_order_amount'] = $completedOrderAmount; - $node['account_cost'] = $effectiveAccountCost; + $node['account_cost'] = $isEligibleForAccountCost ? $effectiveAccountCost : $childrenAccountCost; $node['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount); $node['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount); $node['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount); @@ -1035,9 +1193,61 @@ class ConversionLogic return $total; } - private static function allocateAccountCost(float $globalAccountCost, int $globalAddFansCount, int $rowAddFansCount): float + private static function isAccountCostEligible(string $dimension, array $entity, bool $restrictAccountCostByDept, array $eligibleDeptIds, array $adminToDeptIds): bool { - if ($globalAccountCost <= 0 || $globalAddFansCount <= 0 || $rowAddFansCount <= 0) { + if (!$restrictAccountCostByDept) { + return true; + } + + if ($dimension === 'dept') { + return self::isDeptAccountCostEligible((int) ($entity['id'] ?? 0), true, $eligibleDeptIds); + } + + $adminId = (int) ($entity['id'] ?? 0); + $deptIds = $adminToDeptIds[$adminId] ?? []; + foreach ($deptIds as $deptId) { + if (in_array((int) $deptId, $eligibleDeptIds, true)) { + return true; + } + } + + return false; + } + + private static function isDeptAccountCostEligible(int $deptId, bool $restrictAccountCostByDept, array $eligibleDeptIds): bool + { + if (!$restrictAccountCostByDept) { + return true; + } + + if ($deptId <= 0) { + return false; + } + + return in_array($deptId, $eligibleDeptIds, true); + } + + private static function sumEntityAddFansForAccountCost(array $entities, string $dimension, bool $restrictAccountCostByDept, array $eligibleDeptIds, array $adminToDeptIds): int + { + if (!$restrictAccountCostByDept) { + return self::sumEntityAddFans($entities); + } + + $total = 0; + foreach ($entities as $entity) { + if (!self::isAccountCostEligible($dimension, $entity, true, $eligibleDeptIds, $adminToDeptIds)) { + continue; + } + + $total += (int) ($entity['add_fans_count'] ?? 0); + } + + return $total; + } + + private static function allocateAccountCost(float $globalAccountCost, int $globalAddFansCount, int $rowAddFansCount, bool $isEligible = true): float + { + if (!$isEligible || $globalAccountCost <= 0 || $globalAddFansCount <= 0 || $rowAddFansCount <= 0) { return 0.0; } diff --git a/server/app/adminapi/validate/finance/AccountCostValidate.php b/server/app/adminapi/validate/finance/AccountCostValidate.php index 47c09c66..7da480ed 100644 --- a/server/app/adminapi/validate/finance/AccountCostValidate.php +++ b/server/app/adminapi/validate/finance/AccountCostValidate.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace app\adminapi\validate\finance; +use app\common\model\dept\Dept; use app\common\model\finance\AccountCost; use app\common\service\qywx\MediaChannelService; use app\common\validate\BaseValidate; @@ -14,6 +15,7 @@ class AccountCostValidate extends BaseValidate 'id' => 'require|checkExists', 'cost_date' => 'require|dateFormat:Y-m-d', 'media_channel_code' => 'require|checkMediaChannelCode', + 'dept_id' => 'require|checkDeptId', 'amount' => 'require|float|egt:0', 'remark' => 'max:255', ]; @@ -23,6 +25,7 @@ class AccountCostValidate extends BaseValidate 'cost_date.require' => '请选择日期', 'cost_date.dateFormat' => '日期格式错误', 'media_channel_code.require' => '请选择自媒体渠道', + 'dept_id.require' => '请选择绑定部门', 'amount.require' => '请输入账户消耗金额', 'amount.float' => '账户消耗金额格式错误', 'amount.egt' => '账户消耗金额不能小于0', @@ -31,11 +34,19 @@ class AccountCostValidate extends BaseValidate public function sceneAdd() { + if (!AccountCost::supportsDeptBinding()) { + return $this->remove('id', true)->remove('dept_id', true); + } + return $this->remove('id', true); } public function sceneEdit() { + if (!AccountCost::supportsDeptBinding()) { + return $this->remove('cost_date', true)->remove('dept_id', true); + } + return $this->remove('cost_date', true); } @@ -67,4 +78,22 @@ class AccountCostValidate extends BaseValidate return true; } + + public function checkDeptId($value) + { + if (!AccountCost::supportsDeptBinding()) { + return true; + } + + $deptId = (int) $value; + if ($deptId <= 0) { + return '请选择绑定部门'; + } + + if (!Dept::find($deptId)) { + return '绑定部门不存在'; + } + + return true; + } } diff --git a/server/app/common/model/finance/AccountCost.php b/server/app/common/model/finance/AccountCost.php index a4413975..a417a809 100644 --- a/server/app/common/model/finance/AccountCost.php +++ b/server/app/common/model/finance/AccountCost.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace app\common\model\finance; use app\common\model\BaseModel; +use think\facade\Db; class AccountCost extends BaseModel { @@ -17,4 +18,23 @@ class AccountCost extends BaseModel protected $updateTime = 'update_time'; protected $dateFormat = 'Y-m-d H:i:s'; + + public static function supportsDeptBinding(): bool + { + static $supported = null; + if ($supported !== null) { + return $supported; + } + + try { + $fields = Db::name('account_cost')->getTableFields(); + $supported = is_array($fields) + && in_array('dept_id', $fields, true) + && in_array('dept_name', $fields, true); + } catch (\Throwable) { + $supported = false; + } + + return $supported; + } } diff --git a/server/database/migrations/2026_04_23_create_media_channel_source_and_account_cost_channel.sql b/server/database/migrations/2026_04_23_create_media_channel_source_and_account_cost_channel.sql index 2119b418..f959a1d5 100644 --- a/server/database/migrations/2026_04_23_create_media_channel_source_and_account_cost_channel.sql +++ b/server/database/migrations/2026_04_23_create_media_channel_source_and_account_cost_channel.sql @@ -19,9 +19,11 @@ CREATE TABLE IF NOT EXISTS `zyt_qywx_media_channel` ( ALTER TABLE `zyt_account_cost` ADD COLUMN `media_channel_code` varchar(80) NOT NULL DEFAULT '' COMMENT '渠道编码' AFTER `cost_date`, - ADD COLUMN `media_channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT '渠道名称' AFTER `media_channel_code`; + ADD COLUMN `media_channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT '渠道名称' AFTER `media_channel_code`, + ADD COLUMN `dept_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '绑定部门ID' AFTER `media_channel_name`, + ADD COLUMN `dept_name` varchar(100) NOT NULL DEFAULT '' COMMENT '绑定部门名称' AFTER `dept_id`; ALTER TABLE `zyt_account_cost` DROP INDEX `uk_cost_date`, - ADD UNIQUE KEY `uk_cost_date_channel` (`cost_date`, `media_channel_code`), - ADD KEY `idx_cost_date_channel` (`cost_date`, `media_channel_code`); + ADD UNIQUE KEY `uk_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`), + ADD KEY `idx_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`); diff --git a/server/sql/1.9.20260421/create_account_cost_table.sql b/server/sql/1.9.20260421/create_account_cost_table.sql index 28560479..6fa5520f 100644 --- a/server/sql/1.9.20260421/create_account_cost_table.sql +++ b/server/sql/1.9.20260421/create_account_cost_table.sql @@ -2,6 +2,10 @@ CREATE TABLE IF NOT EXISTS `zyt_account_cost` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `cost_date` date NOT NULL COMMENT '账户消耗日期', `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '账户消耗金额', + `dept_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '绑定部门ID', + `dept_name` varchar(100) NOT NULL DEFAULT '' COMMENT '绑定部门名称', + `media_channel_code` varchar(80) NOT NULL DEFAULT '' COMMENT '渠道编码', + `media_channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT '渠道名称', `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注', `creator_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人ID', `creator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人姓名', @@ -10,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `zyt_account_cost` ( `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_cost_date` (`cost_date`), + UNIQUE KEY `uk_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`), + KEY `idx_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`), KEY `idx_update_time` (`update_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户消耗表'; diff --git a/server/sql/1.9.20260423/create_media_channel_source_and_account_cost_channel.sql b/server/sql/1.9.20260423/create_media_channel_source_and_account_cost_channel.sql index 71bde34f..780e0cb8 100644 --- a/server/sql/1.9.20260423/create_media_channel_source_and_account_cost_channel.sql +++ b/server/sql/1.9.20260423/create_media_channel_source_and_account_cost_channel.sql @@ -19,12 +19,14 @@ CREATE TABLE IF NOT EXISTS `zyt_qywx_media_channel` ( ALTER TABLE `zyt_account_cost` ADD COLUMN `media_channel_code` varchar(80) NOT NULL DEFAULT '' COMMENT '渠道编码' AFTER `cost_date`, - ADD COLUMN `media_channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT '渠道名称' AFTER `media_channel_code`; + ADD COLUMN `media_channel_name` varchar(100) NOT NULL DEFAULT '' COMMENT '渠道名称' AFTER `media_channel_code`, + ADD COLUMN `dept_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '绑定部门ID' AFTER `media_channel_name`, + ADD COLUMN `dept_name` varchar(100) NOT NULL DEFAULT '' COMMENT '绑定部门名称' AFTER `dept_id`; ALTER TABLE `zyt_account_cost` DROP INDEX `uk_cost_date`, - ADD UNIQUE KEY `uk_cost_date_channel` (`cost_date`, `media_channel_code`), - ADD KEY `idx_cost_date_channel` (`cost_date`, `media_channel_code`); + ADD UNIQUE KEY `uk_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`), + ADD KEY `idx_cost_date_channel_dept` (`cost_date`, `media_channel_code`, `dept_id`); -- 初始化渠道源 -- php think qywx:scan-media-channel