From a89c7cce6289aad166a1a5a0060ae53ad305f18c Mon Sep 17 00:00:00 2001
From: ShengLong <452591453@qq.com>
Date: Mon, 27 Apr 2026 12:50:35 +0800
Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=AA=92=E4=BD=93=E6=B8=A0=E9=81=93?=
=?UTF-8?q?=E7=BB=91=E5=AE=9A=E9=83=A8=E9=97=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
admin/src/views/finance/account_cost/edit.vue | 36 +++
.../src/views/finance/account_cost/index.vue | 35 +++
.../lists/finance/AccountCostLists.php | 14 +-
.../logic/finance/AccountCostLogic.php | 71 ++++-
.../adminapi/logic/stats/ConversionLogic.php | 272 ++++++++++++++++--
.../validate/finance/AccountCostValidate.php | 29 ++
.../app/common/model/finance/AccountCost.php | 20 ++
...hannel_source_and_account_cost_channel.sql | 8 +-
.../create_account_cost_table.sql | 7 +-
...hannel_source_and_account_cost_channel.sql | 8 +-
10 files changed, 457 insertions(+), 43 deletions(-)
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 @@
/>
+
+
+
+
¥{{ row.amount }}
@@ -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