feat(stats): 新增自录转化统计模块

支持后台用户手动录入个人业绩与账户消耗,独立计算转化指标,接入 DataScope 数据权限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 09:17:16 +08:00
co-authored by Cursor
parent 49c8c95966
commit 183b6a1acf
21 changed files with 2201 additions and 0 deletions
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\PersonalAccountCostLists;
use app\adminapi\logic\stats\PersonalAccountCostLogic;
use app\adminapi\validate\stats\PersonalAccountCostValidate;
class PersonalAccountCostController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new PersonalAccountCostLists());
}
public function add()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('add');
$result = PersonalAccountCostLogic::add($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''));
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('添加成功', [], 1, 1);
}
public function edit()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('edit');
$result = PersonalAccountCostLogic::edit($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''), $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('编辑成功', [], 1, 1);
}
public function detail()
{
$params = (new PersonalAccountCostValidate())->goCheck('detail');
$detail = PersonalAccountCostLogic::detail((int) $params['id'], $this->adminId, $this->adminInfo);
if ($detail === []) {
return $this->fail('记录不存在或无权查看');
}
return $this->data($detail);
}
public function delete()
{
$params = (new PersonalAccountCostValidate())->post()->goCheck('delete');
$result = PersonalAccountCostLogic::delete((int) $params['id'], $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalAccountCostLogic::getError());
}
return $this->success('删除成功', [], 1, 1);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\PersonalYejiLists;
use app\adminapi\logic\stats\PersonalYejiLogic;
use app\adminapi\validate\stats\PersonalYejiValidate;
class PersonalYejiController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new PersonalYejiLists());
}
public function add()
{
$params = (new PersonalYejiValidate())->post()->goCheck('add');
$result = PersonalYejiLogic::add($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''));
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('添加成功', [], 1, 1);
}
public function edit()
{
$params = (new PersonalYejiValidate())->post()->goCheck('edit');
$result = PersonalYejiLogic::edit($params, $this->adminId, (string) ($this->adminInfo['name'] ?? ''), $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('编辑成功', [], 1, 1);
}
public function detail()
{
$params = (new PersonalYejiValidate())->goCheck('detail');
$detail = PersonalYejiLogic::detail((int) $params['id'], $this->adminId, $this->adminInfo);
if ($detail === []) {
return $this->fail('记录不存在或无权查看');
}
return $this->data($detail);
}
public function delete()
{
$params = (new PersonalYejiValidate())->post()->goCheck('delete');
$result = PersonalYejiLogic::delete((int) $params['id'], $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PersonalYejiLogic::getError());
}
return $this->success('删除成功', [], 1, 1);
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\SelfInputLogic;
class SelfInputController extends BaseAdminController
{
public function overview()
{
$result = SelfInputLogic::overview($this->request->get(), $this->adminId, $this->adminInfo);
return $this->data($result);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\stats\PersonalAccountCost;
class PersonalAccountCostLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
{
use HasDataScopeFilter;
public function setSearch(): array
{
return [
'%like%' => ['media_source', 'creator_name', 'remark'],
];
}
private function baseQuery()
{
$query = PersonalAccountCost::where($this->searchWhere);
$this->applyDataScopeByOwner($query, 'creator_id');
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('cost_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('cost_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('cost_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['media_source'])) {
$query->whereLike('media_source', '%' . trim((string) $this->params['media_source']) . '%');
}
return $query;
}
public function lists(): array
{
return $this->baseQuery()
->order(['cost_date' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
public function count(): int
{
return (int) $this->baseQuery()->count();
}
public function extend(): array
{
return [
'total_amount' => round((float) $this->baseQuery()->sum('amount'), 2),
'days_count' => (int) $this->baseQuery()->distinct(true)->count('cost_date'),
];
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
use app\common\model\stats\PersonalYeji;
class PersonalYejiLists extends BaseAdminDataLists implements ListsSearchInterface
{
use HasDataScopeFilter;
public function setSearch(): array
{
return [
'%like%' => ['media_source', 'creator_name', 'remark'],
];
}
private function baseQuery()
{
$query = PersonalYeji::where($this->searchWhere);
$this->applyDataScopeByOwner($query, 'creator_id');
if (!empty($this->params['start_date']) && !empty($this->params['end_date'])) {
$query->whereBetween('yeji_date', [$this->params['start_date'], $this->params['end_date']]);
} elseif (!empty($this->params['start_date'])) {
$query->where('yeji_date', '>=', $this->params['start_date']);
} elseif (!empty($this->params['end_date'])) {
$query->where('yeji_date', '<=', $this->params['end_date']);
}
if (!empty($this->params['media_source'])) {
$query->whereLike('media_source', '%' . trim((string) $this->params['media_source']) . '%');
}
if (!empty($this->params['creator_id'])) {
$query->where('creator_id', (int) $this->params['creator_id']);
}
return $query;
}
public function lists(): array
{
return $this->baseQuery()
->order(['yeji_date' => 'desc', 'id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
}
public function count(): int
{
return (int) $this->baseQuery()->count();
}
}
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\logic\BaseLogic;
use app\common\model\stats\PersonalAccountCost;
class PersonalAccountCostLogic extends BaseLogic
{
use PersonalStatsScopeTrait;
public static function add(array $params, int $adminId, string $adminName): bool
{
try {
$costDate = (string) $params['cost_date'];
$mediaSource = self::normalizeMediaSource((string) ($params['media_source'] ?? ''));
if ($mediaSource === '') {
self::setError('请填写自媒体来源');
return false;
}
if (self::isCostDuplicate($adminId, $costDate, $mediaSource)) {
self::setError('该日期下该自媒体来源的账户消耗已存在,请直接编辑');
return false;
}
PersonalAccountCost::create([
'cost_date' => $costDate,
'media_source' => $mediaSource,
'amount' => round((float) ($params['amount'] ?? 0), 2),
'remark' => (string) ($params['remark'] ?? ''),
'creator_id' => $adminId,
'creator_name' => $adminName,
'updater_id' => $adminId,
'updater_name' => $adminName,
'dept_id' => self::resolvePrimaryDeptId($adminId),
]);
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function edit(array $params, int $adminId, string $adminName, array $adminInfo): bool
{
try {
$model = PersonalAccountCost::find($params['id']);
if (!$model) {
self::setError('记录不存在');
return false;
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('无权操作该记录');
return false;
}
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('仅可编辑本人录入的账户消耗');
return false;
}
$model->amount = round((float) ($params['amount'] ?? 0), 2);
$model->remark = (string) ($params['remark'] ?? '');
$model->updater_id = $adminId;
$model->updater_name = $adminName;
$model->save();
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function delete(int $id, int $adminId, array $adminInfo): bool
{
try {
$model = PersonalAccountCost::find($id);
if (!$model) {
self::setError('记录不存在');
return false;
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('无权操作该记录');
return false;
}
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('仅可删除本人录入的账户消耗');
return false;
}
$model->delete();
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function detail(int $id, int $adminId, array $adminInfo): array
{
$model = PersonalAccountCost::find($id);
if (!$model) {
return [];
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
return [];
}
return $model->toArray();
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\model\auth\AdminDept;
use app\common\model\stats\PersonalAccountCost;
use app\common\model\stats\PersonalYeji;
use app\common\service\DataScope\DataScopeService;
trait PersonalStatsScopeTrait
{
protected static function resolvePrimaryDeptId(int $adminId): int
{
if ($adminId <= 0) {
return 0;
}
$deptId = AdminDept::where('admin_id', $adminId)->value('dept_id');
return (int) ($deptId ?: 0);
}
protected static function normalizeMediaSource(string $mediaSource): string
{
return trim($mediaSource);
}
/**
* @return array<int>|null
*/
protected static function getVisibleCreatorIds(int $adminId, array $adminInfo): ?array
{
return DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
}
protected static function canMutateRecord(int $adminId, array $adminInfo, int $creatorId): bool
{
if ((int) ($adminInfo['root'] ?? 0) === 1) {
return true;
}
return $adminId > 0 && $adminId === $creatorId;
}
protected static function assertRecordVisible(int $adminId, array $adminInfo, int $creatorId): bool
{
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
if ($visibleIds === null) {
return true;
}
return in_array($creatorId, $visibleIds, true);
}
protected static function isYejiDuplicate(int $creatorId, string $yejiDate, string $mediaSource, int $excludeId = 0): bool
{
$query = PersonalYeji::where('creator_id', $creatorId)
->where('yeji_date', $yejiDate)
->where('media_source', $mediaSource);
if ($excludeId > 0) {
$query->where('id', '<>', $excludeId);
}
return $query->count() > 0;
}
protected static function isCostDuplicate(int $creatorId, string $costDate, string $mediaSource, int $excludeId = 0): bool
{
$query = PersonalAccountCost::where('creator_id', $creatorId)
->where('cost_date', $costDate)
->where('media_source', $mediaSource);
if ($excludeId > 0) {
$query->where('id', '<>', $excludeId);
}
return $query->count() > 0;
}
}
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\logic\BaseLogic;
use app\common\model\stats\PersonalYeji;
class PersonalYejiLogic extends BaseLogic
{
use PersonalStatsScopeTrait;
public static function add(array $params, int $adminId, string $adminName): bool
{
try {
$yejiDate = (string) $params['yeji_date'];
$mediaSource = self::normalizeMediaSource((string) ($params['media_source'] ?? ''));
if ($mediaSource === '') {
self::setError('请填写自媒体来源');
return false;
}
if (self::isYejiDuplicate($adminId, $yejiDate, $mediaSource)) {
self::setError('该日期下该自媒体来源的业绩已存在,请直接编辑');
return false;
}
PersonalYeji::create([
'yeji_date' => $yejiDate,
'media_source' => $mediaSource,
'add_fans_count' => (int) ($params['add_fans_count'] ?? 0),
'total_open_count' => (int) ($params['total_open_count'] ?? 0),
'unreplied_count' => (int) ($params['unreplied_count'] ?? 0),
'paid_appointment_count' => (int) ($params['paid_appointment_count'] ?? 0),
'free_appointment_count' => (int) ($params['free_appointment_count'] ?? 0),
'interview_count' => (int) ($params['interview_count'] ?? 0),
'order_amount' => round((float) ($params['order_amount'] ?? 0), 2),
'completed_order_count' => (int) ($params['completed_order_count'] ?? 0),
'remark' => (string) ($params['remark'] ?? ''),
'creator_id' => $adminId,
'creator_name' => $adminName,
'updater_id' => $adminId,
'updater_name' => $adminName,
'dept_id' => self::resolvePrimaryDeptId($adminId),
]);
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function edit(array $params, int $adminId, string $adminName, array $adminInfo): bool
{
try {
$model = PersonalYeji::find($params['id']);
if (!$model) {
self::setError('记录不存在');
return false;
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('无权操作该记录');
return false;
}
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('仅可编辑本人录入的业绩');
return false;
}
$model->add_fans_count = (int) ($params['add_fans_count'] ?? 0);
$model->total_open_count = (int) ($params['total_open_count'] ?? 0);
$model->unreplied_count = (int) ($params['unreplied_count'] ?? 0);
$model->paid_appointment_count = (int) ($params['paid_appointment_count'] ?? 0);
$model->free_appointment_count = (int) ($params['free_appointment_count'] ?? 0);
$model->interview_count = (int) ($params['interview_count'] ?? 0);
$model->order_amount = round((float) ($params['order_amount'] ?? 0), 2);
$model->completed_order_count = (int) ($params['completed_order_count'] ?? 0);
$model->remark = (string) ($params['remark'] ?? '');
$model->updater_id = $adminId;
$model->updater_name = $adminName;
$model->save();
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function delete(int $id, int $adminId, array $adminInfo): bool
{
try {
$model = PersonalYeji::find($id);
if (!$model) {
self::setError('记录不存在');
return false;
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('无权操作该记录');
return false;
}
if (!self::canMutateRecord($adminId, $adminInfo, (int) $model->creator_id)) {
self::setError('仅可删除本人录入的业绩');
return false;
}
$model->delete();
return true;
} catch (\Throwable $e) {
self::setError($e->getMessage());
return false;
}
}
public static function detail(int $id, int $adminId, array $adminInfo): array
{
$model = PersonalYeji::find($id);
if (!$model) {
return [];
}
if (!self::assertRecordVisible($adminId, $adminInfo, (int) $model->creator_id)) {
return [];
}
return $model->toArray();
}
}
@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\logic\BaseLogic;
use app\common\model\stats\PersonalAccountCost;
use app\common\model\stats\PersonalYeji;
class SelfInputLogic extends BaseLogic
{
use PersonalStatsScopeTrait;
public static function overview(array $params, int $adminId, array $adminInfo): array
{
[$startDate, $endDate] = self::resolveTimeRange($params);
$pageNo = max(1, (int) ($params['page_no'] ?? 1));
$pageSize = min(100, max(1, (int) ($params['page_size'] ?? 15)));
$mediaSource = trim((string) ($params['media_source'] ?? ''));
$yejiQuery = self::buildYejiQuery($startDate, $endDate, $adminId, $adminInfo);
if ($mediaSource !== '') {
$yejiQuery->whereLike('media_source', '%' . $mediaSource . '%');
}
$count = (int) (clone $yejiQuery)->count();
$rows = (clone $yejiQuery)
->order(['yeji_date' => 'desc', 'id' => 'desc'])
->page($pageNo, $pageSize)
->select()
->toArray();
$costMap = self::loadAccountCostMap($startDate, $endDate, $adminId, $adminInfo, $mediaSource);
$lists = [];
foreach ($rows as $row) {
$entity = self::normalizeYejiRow($row);
$costKey = self::buildCostKey(
(int) $entity['creator_id'],
(string) $entity['yeji_date'],
(string) $entity['media_source']
);
$entity['account_cost'] = round((float) ($costMap[$costKey] ?? 0), 2);
$lists[] = self::finalizeMetrics($entity);
}
$allYejiRows = self::buildYejiQuery($startDate, $endDate, $adminId, $adminInfo);
if ($mediaSource !== '') {
$allYejiRows->whereLike('media_source', '%' . $mediaSource . '%');
}
$allYeji = $allYejiRows->select()->toArray();
$summaryCostMap = $costMap;
$summaryBase = self::emptyMetrics();
foreach ($allYeji as $row) {
$entity = self::normalizeYejiRow($row);
$costKey = self::buildCostKey(
(int) $entity['creator_id'],
(string) $entity['yeji_date'],
(string) $entity['media_source']
);
$entity['account_cost'] = round((float) ($summaryCostMap[$costKey] ?? 0), 2);
unset($summaryCostMap[$costKey]);
$entity = self::finalizeMetrics($entity);
$summaryBase = self::accumulateMetrics($summaryBase, $entity);
}
foreach ($summaryCostMap as $amount) {
$summaryBase['account_cost'] += round((float) $amount, 2);
}
$summary = self::finalizeMetrics($summaryBase);
return [
'summary' => $summary,
'lists' => $lists,
'count' => $count,
'page_no' => $pageNo,
'page_size' => $pageSize,
'extend' => [
'summary' => $summary,
'date_range' => [$startDate, $endDate],
],
];
}
private static function buildYejiQuery(string $startDate, string $endDate, int $adminId, array $adminInfo)
{
$query = PersonalYeji::whereBetween('yeji_date', [$startDate, $endDate]);
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
if ($visibleIds === []) {
$query->whereRaw('0 = 1');
} elseif ($visibleIds !== null) {
$query->whereIn('creator_id', $visibleIds);
}
return $query;
}
/**
* @return array<string, float>
*/
private static function loadAccountCostMap(
string $startDate,
string $endDate,
int $adminId,
array $adminInfo,
string $mediaSource
): array {
$query = PersonalAccountCost::whereBetween('cost_date', [$startDate, $endDate]);
$visibleIds = self::getVisibleCreatorIds($adminId, $adminInfo);
if ($visibleIds === []) {
return [];
}
if ($visibleIds !== null) {
$query->whereIn('creator_id', $visibleIds);
}
if ($mediaSource !== '') {
$query->whereLike('media_source', '%' . $mediaSource . '%');
}
$rows = $query
->fieldRaw('creator_id, cost_date, media_source, SUM(amount) AS total_amount')
->group('creator_id, cost_date, media_source')
->select()
->toArray();
$map = [];
foreach ($rows as $row) {
$key = self::buildCostKey(
(int) $row['creator_id'],
(string) $row['cost_date'],
(string) $row['media_source']
);
$map[$key] = round((float) ($row['total_amount'] ?? 0), 2);
}
return $map;
}
private static function buildCostKey(int $creatorId, string $date, string $mediaSource): string
{
return $creatorId . '|' . $date . '|' . self::normalizeMediaSource($mediaSource);
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private static function normalizeYejiRow(array $row): array
{
return [
'id' => (int) ($row['id'] ?? 0),
'yeji_date' => (string) ($row['yeji_date'] ?? ''),
'media_source' => (string) ($row['media_source'] ?? ''),
'creator_id' => (int) ($row['creator_id'] ?? 0),
'creator_name' => (string) ($row['creator_name'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
'add_fans_count' => (int) ($row['add_fans_count'] ?? 0),
'total_open_count' => (int) ($row['total_open_count'] ?? 0),
'unreplied_count' => (int) ($row['unreplied_count'] ?? 0),
'paid_appointment_count' => (int) ($row['paid_appointment_count'] ?? 0),
'free_appointment_count' => (int) ($row['free_appointment_count'] ?? 0),
'interview_count' => (int) ($row['interview_count'] ?? 0),
'order_amount' => round((float) ($row['order_amount'] ?? 0), 2),
'completed_order_count' => (int) ($row['completed_order_count'] ?? 0),
'account_cost' => 0.0,
];
}
/**
* @return array<string, mixed>
*/
private static function emptyMetrics(): array
{
return [
'add_fans_count' => 0,
'total_open_count' => 0,
'unreplied_count' => 0,
'paid_appointment_count' => 0,
'free_appointment_count' => 0,
'appointment_total_count' => 0,
'interview_count' => 0,
'order_amount' => 0.0,
'completed_order_count' => 0,
'account_cost' => 0.0,
];
}
/**
* @param array<string, mixed> $base
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private static function accumulateMetrics(array $base, array $row): array
{
$base['add_fans_count'] += (int) ($row['add_fans_count'] ?? 0);
$base['total_open_count'] += (int) ($row['total_open_count'] ?? 0);
$base['unreplied_count'] += (int) ($row['unreplied_count'] ?? 0);
$base['paid_appointment_count'] += (int) ($row['paid_appointment_count'] ?? 0);
$base['free_appointment_count'] += (int) ($row['free_appointment_count'] ?? 0);
$base['interview_count'] += (int) ($row['interview_count'] ?? 0);
$base['order_amount'] = round((float) $base['order_amount'] + (float) ($row['order_amount'] ?? 0), 2);
$base['completed_order_count'] += (int) ($row['completed_order_count'] ?? 0);
$base['account_cost'] = round((float) $base['account_cost'] + (float) ($row['account_cost'] ?? 0), 2);
return $base;
}
/**
* @param array<string, mixed> $entity
* @return array<string, mixed>
*/
private static function finalizeMetrics(array $entity): array
{
$paidAppointmentCount = (int) ($entity['paid_appointment_count'] ?? 0);
$freeAppointmentCount = (int) ($entity['free_appointment_count'] ?? 0);
$appointmentTotalCount = $paidAppointmentCount + $freeAppointmentCount;
$interviewCount = (int) ($entity['interview_count'] ?? 0);
$addFansCount = (int) ($entity['add_fans_count'] ?? 0);
$totalOpenCount = (int) ($entity['total_open_count'] ?? 0);
$completedOrderCount = (int) ($entity['completed_order_count'] ?? 0);
$orderAmount = round((float) ($entity['order_amount'] ?? 0), 2);
$accountCost = round((float) ($entity['account_cost'] ?? 0), 2);
$entity['appointment_total_count'] = $appointmentTotalCount;
$entity['order_amount'] = $orderAmount;
$entity['account_cost'] = $accountCost;
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$entity['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$entity['avg_unit_price'] = self::safeDivideMoney($orderAmount, $completedOrderCount);
$entity['cash_cost'] = self::safeDivideMoney($accountCost, $addFansCount);
$entity['roi'] = self::safeDivideRatio($orderAmount, $accountCost);
return $entity;
}
/**
* @return array{0: string, 1: string}
*/
private static function resolveTimeRange(array $params): array
{
$today = date('Y-m-d');
$timeType = (string) ($params['time_type'] ?? 'today');
switch ($timeType) {
case 'yesterday':
$startDate = date('Y-m-d', strtotime('-1 day'));
$endDate = $startDate;
break;
case 'week':
$startDate = date('Y-m-d', strtotime('-6 days'));
$endDate = $today;
break;
case 'month':
$startDate = date('Y-m-d', strtotime('-29 days'));
$endDate = $today;
break;
case 'custom':
$startDate = trim((string) ($params['start_date'] ?? ''));
$endDate = trim((string) ($params['end_date'] ?? ''));
if ($startDate === '' || $endDate === '') {
$startDate = $today;
$endDate = $today;
} elseif ($startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
break;
case 'today':
default:
$startDate = $today;
$endDate = $today;
}
return [$startDate, $endDate];
}
private static function percent(int $numerator, int $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round(($numerator / $denominator) * 100, 2);
}
private static function safeDivideMoney(float $numerator, int $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round($numerator / $denominator, 2);
}
private static function safeDivideRatio(float $numerator, float $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round($numerator / $denominator, 2);
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace app\adminapi\validate\stats;
use app\common\model\stats\PersonalAccountCost;
use app\common\validate\BaseValidate;
class PersonalAccountCostValidate extends BaseValidate
{
protected $rule = [
'id' => 'require|checkExists',
'cost_date' => 'require|dateFormat:Y-m-d',
'media_source' => 'require|max:120',
'amount' => 'require|float|egt:0',
'remark' => 'max:255',
];
protected $message = [
'id.require' => '参数缺失',
'cost_date.require' => '请选择日期',
'cost_date.dateFormat' => '日期格式错误',
'media_source.require' => '请填写自媒体来源',
'media_source.max' => '自媒体来源不能超过120个字符',
'amount.require' => '请输入账户消耗金额',
'amount.float' => '账户消耗金额格式错误',
'amount.egt' => '账户消耗金额不能小于0',
'remark.max' => '备注不能超过255个字符',
];
public function sceneAdd()
{
return $this->remove('id', true);
}
public function sceneEdit()
{
return $this->remove('cost_date', true)->remove('media_source', true);
}
public function sceneDetail()
{
return $this->only(['id']);
}
public function sceneDelete()
{
return $this->only(['id']);
}
public function checkExists($value)
{
$model = PersonalAccountCost::find($value);
if (!$model) {
return '记录不存在';
}
return true;
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace app\adminapi\validate\stats;
use app\common\model\stats\PersonalYeji;
use app\common\validate\BaseValidate;
class PersonalYejiValidate extends BaseValidate
{
protected $rule = [
'id' => 'require|checkExists',
'yeji_date' => 'require|dateFormat:Y-m-d',
'media_source' => 'require|max:120',
'add_fans_count' => 'require|integer|egt:0',
'total_open_count' => 'integer|egt:0',
'unreplied_count' => 'integer|egt:0',
'paid_appointment_count' => 'integer|egt:0',
'free_appointment_count' => 'integer|egt:0',
'interview_count' => 'integer|egt:0',
'order_amount' => 'float|egt:0',
'completed_order_count' => 'integer|egt:0',
'remark' => 'max:255',
];
protected $message = [
'id.require' => '参数缺失',
'yeji_date.require' => '请选择业绩日期',
'yeji_date.dateFormat' => '业绩日期格式错误',
'media_source.require' => '请填写自媒体来源',
'media_source.max' => '自媒体来源不能超过120个字符',
'add_fans_count.require' => '请填写加粉数',
'add_fans_count.integer' => '加粉数格式错误',
'add_fans_count.egt' => '加粉数不能小于0',
'remark.max' => '备注不能超过255个字符',
];
public function sceneAdd()
{
return $this->remove('id', true);
}
public function sceneEdit()
{
return $this->remove('yeji_date', true)->remove('media_source', true);
}
public function sceneDetail()
{
return $this->only(['id']);
}
public function sceneDelete()
{
return $this->only(['id']);
}
public function checkExists($value)
{
$model = PersonalYeji::find($value);
if (!$model) {
return '记录不存在';
}
return true;
}
}