feat: 小程序与管理端迭代(神仙AI、了解自己CRM、AI测试入口、报表与分润等)
Made-with: Cursor
This commit is contained in:
@@ -31,6 +31,14 @@ class AppUser extends BaseController
|
||||
$pageSize = (int) Request::param('pageSize', 20);
|
||||
$pageSize = min(max($pageSize, 1), 100);
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
$coldFaceLevelRaw = Request::param('coldFaceLevel', '');
|
||||
$coldFaceLevels = [];
|
||||
if (is_array($coldFaceLevelRaw)) {
|
||||
$coldFaceLevels = array_values(array_filter(array_map('strval', $coldFaceLevelRaw)));
|
||||
} elseif (is_string($coldFaceLevelRaw) && $coldFaceLevelRaw !== '') {
|
||||
$coldFaceLevels = array_values(array_filter(array_map('trim', explode(',', $coldFaceLevelRaw))));
|
||||
}
|
||||
$coldFaceLevels = array_values(array_intersect($coldFaceLevels, ['cold', 'neutral', 'warm']));
|
||||
|
||||
$where = [];
|
||||
if ($keyword !== '') {
|
||||
@@ -72,6 +80,19 @@ class AppUser extends BaseController
|
||||
->join([$poolSql => 'p'], 'w.id = p.userId')
|
||||
->join([$dedupSql => 'd'], 'w.id = d.mid');
|
||||
|
||||
if (!empty($coldFaceLevels)) {
|
||||
try {
|
||||
$cfSql = Db::name('user_profile')
|
||||
->whereRaw('enterpriseId = ' . $eid)
|
||||
->whereIn('coldFaceLevel', $coldFaceLevels)
|
||||
->field('userId')
|
||||
->buildSql(true);
|
||||
$baseQuery->join([$cfSql => 'cf'], 'w.id = cf.userId');
|
||||
} catch (\Throwable $e) {
|
||||
// coldFace 字段不存在时忽略筛选
|
||||
}
|
||||
}
|
||||
|
||||
if ($keyword !== '') {
|
||||
$like = '%' . addcslashes($keyword, '%_\\') . '%';
|
||||
$baseQuery->whereRaw(
|
||||
@@ -215,6 +236,31 @@ class AppUser extends BaseController
|
||||
} catch (\Throwable $e) {
|
||||
$payStats = [];
|
||||
}
|
||||
|
||||
// 冷脸分字段(容错:迁移未执行时 coldFace* 字段缺失,查询异常则视为全空)
|
||||
$coldFaceMap = [];
|
||||
try {
|
||||
$cfQuery = Db::name('user_profile')->where('userId', 'in', $ids);
|
||||
if ($enterpriseId) {
|
||||
$cfQuery->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$cfRows = $cfQuery
|
||||
->field('userId, coldFaceScore, coldFaceLevel, coldFaceUpdatedAt')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($cfRows as $r) {
|
||||
$uid = (int) ($r['userId'] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$coldFaceMap[$uid] = [
|
||||
'score' => isset($r['coldFaceScore']) && $r['coldFaceScore'] !== null ? (int) $r['coldFaceScore'] : null,
|
||||
'level' => $r['coldFaceLevel'] ?? null,
|
||||
'updatedAt' => isset($r['coldFaceUpdatedAt']) ? (int) $r['coldFaceUpdatedAt'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$coldFaceMap = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
@@ -236,6 +282,18 @@ class AppUser extends BaseController
|
||||
$pay = $payStats[$id] ?? null;
|
||||
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
|
||||
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
|
||||
|
||||
$cf = $coldFaceMap[$id] ?? null;
|
||||
if ((!$cf || $cf['score'] === null) && !empty($testsForUser)) {
|
||||
$calc = $this->calcColdFace($testsForUser);
|
||||
if ($calc) {
|
||||
$this->writeColdFace((int) $id, $enterpriseId ? (int) $enterpriseId : null, $calc);
|
||||
$cf = ['score' => $calc['score'], 'level' => $calc['level'], 'updatedAt' => time()];
|
||||
}
|
||||
}
|
||||
$row['coldFaceScore'] = $cf && $cf['score'] !== null ? (int) $cf['score'] : null;
|
||||
$row['coldFaceLevel'] = $cf && !empty($cf['level']) ? (string) $cf['level'] : null;
|
||||
$row['coldFaceUpdatedAt'] = $cf ? ($cf['updatedAt'] ?? null) : null;
|
||||
}
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
@@ -322,6 +380,35 @@ class AppUser extends BaseController
|
||||
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
|
||||
$data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp');
|
||||
|
||||
// 冷脸分字段(详情)
|
||||
$coldFace = null;
|
||||
try {
|
||||
$cfQuery = Db::name('user_profile')->where('userId', $id);
|
||||
if ($enterpriseId) {
|
||||
$cfQuery->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$cfRow = $cfQuery->field('coldFaceScore, coldFaceLevel, coldFaceUpdatedAt')->find();
|
||||
if ($cfRow) {
|
||||
$coldFace = [
|
||||
'score' => $cfRow['coldFaceScore'] !== null ? (int) $cfRow['coldFaceScore'] : null,
|
||||
'level' => $cfRow['coldFaceLevel'] ?? null,
|
||||
'updatedAt' => isset($cfRow['coldFaceUpdatedAt']) ? (int) $cfRow['coldFaceUpdatedAt'] : null,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$coldFace = null;
|
||||
}
|
||||
if ((!$coldFace || $coldFace['score'] === null) && !empty($tests)) {
|
||||
$calc = $this->calcColdFace($tests);
|
||||
if ($calc) {
|
||||
$this->writeColdFace((int) $id, $enterpriseId ? (int) $enterpriseId : null, $calc);
|
||||
$coldFace = ['score' => $calc['score'], 'level' => $calc['level'], 'updatedAt' => time()];
|
||||
}
|
||||
}
|
||||
$data['coldFaceScore'] = $coldFace['score'] ?? null;
|
||||
$data['coldFaceLevel'] = $coldFace['level'] ?? null;
|
||||
$data['coldFaceUpdatedAt'] = $coldFace['updatedAt'] ?? null;
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Finance extends BaseController
|
||||
$baseOrderQuery = Db::name('orders')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->whereIn('status', ['paid', 'completed'])
|
||||
->whereIn('productType', ['face', 'mbti', 'sbti', 'disc', 'pdp']);
|
||||
->whereIn('productType', ['face', 'mbti', 'sbti', 'disc', 'pdp', 'ai_deep_report']);
|
||||
|
||||
$totalIncomeFen = (int) ((clone $baseOrderQuery)->sum('amount') ?? 0);
|
||||
$todayIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $todayStart)->sum('amount') ?? 0);
|
||||
@@ -54,16 +54,44 @@ class Finance extends BaseController
|
||||
->where('status', 'frozen')
|
||||
->sum('commissionFen') ?? 0);
|
||||
|
||||
// 本月累计扣款(平台扣费 + 佣金扣减)
|
||||
$monthConsumeFen = (int) (Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('type', 'consume')
|
||||
->where('createdAt', '>=', $monthStart)
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
// 今日扣款
|
||||
$todayConsumeFen = (int) (Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('type', 'consume')
|
||||
->where('createdAt', '>=', $todayStart)
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
// 建议充值:按本月实际天数日均 × 14 天,低于 500 元按 500 元兜底;余额足够则 0
|
||||
$balanceFen = (int) ($enterprise['balance'] ?? 0);
|
||||
$daysElapsed = max(1, (int) date('j'));
|
||||
$avgDaily = $monthConsumeFen > 0 ? intval($monthConsumeFen / $daysElapsed) : 0;
|
||||
$suggestThreshold = $avgDaily * 14;
|
||||
$suggestRechargeFen = 0;
|
||||
if ($avgDaily > 0 && $balanceFen < $suggestThreshold) {
|
||||
$suggestRechargeFen = max($suggestThreshold - $balanceFen, 50000); // ≥ 500 元
|
||||
}
|
||||
|
||||
return success([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseName' => $enterprise['name'] ?? '',
|
||||
'balanceFen' => (int) ($enterprise['balance'] ?? 0),
|
||||
'balanceFen' => $balanceFen,
|
||||
'totalIncomeFen' => $totalIncomeFen,
|
||||
'todayIncomeFen' => $todayIncomeFen,
|
||||
'monthIncomeFen' => $monthIncomeFen,
|
||||
'manualRechargeFen' => $manualRechargeFen,
|
||||
'frozenCommissionFen' => $frozenCommissionFen,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
'monthConsumeFen' => $monthConsumeFen,
|
||||
'todayConsumeFen' => $todayConsumeFen,
|
||||
'avgDailyConsumeFen' => $avgDaily,
|
||||
'suggestRechargeFen' => $suggestRechargeFen,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业财务概览失败:' . $e->getMessage(), 500);
|
||||
|
||||
@@ -31,6 +31,7 @@ class Order extends BaseController
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
$status = trim(Request::param('status', ''));
|
||||
$productType = trim(Request::param('productType', ''));
|
||||
$inviterId = (int) Request::param('inviterId', 0);
|
||||
|
||||
// 超管:全平台订单;其余管理员仅本企业
|
||||
$enterpriseId = null;
|
||||
@@ -53,6 +54,24 @@ class Order extends BaseController
|
||||
if ($productType !== '') {
|
||||
$query->where('productType', $productType);
|
||||
}
|
||||
if ($inviterId > 0) {
|
||||
// 通过分销绑定表过滤「该分销商带来的订单」:orders.userId = distribution_bindings.inviteeId,inviter=inviterId
|
||||
try {
|
||||
$inviteeIds = Db::name('distribution_bindings')
|
||||
->where('inviterId', $inviterId)
|
||||
->column('inviteeId');
|
||||
$inviteeIds = array_values(array_filter(array_map('intval', $inviteeIds ?: [])));
|
||||
if (empty($inviteeIds)) {
|
||||
return success([
|
||||
'list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize,
|
||||
'hasMore' => false, 'paidCompletedCount' => 0, 'totalRevenueFen' => 0,
|
||||
]);
|
||||
}
|
||||
$query->whereIn('userId', $inviteeIds);
|
||||
} catch (\Throwable $e) {
|
||||
// 表不存在或字段差异:忽略筛选
|
||||
}
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
if (is_numeric($keyword)) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
@@ -119,12 +138,58 @@ class Order extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
// 当前页订单的分销分润记录
|
||||
$commissionsByOrder = [];
|
||||
if (!empty($orderIds)) {
|
||||
try {
|
||||
$crRows = Db::name('commission_records')
|
||||
->where('orderId', 'in', $orderIds)
|
||||
->field('id, orderId, inviterId, commissionFen, rate, status, paidAt, createdAt')
|
||||
->order('createdAt', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$inviterIds = array_values(array_unique(array_filter(array_column($crRows, 'inviterId'))));
|
||||
$inviterMap = [];
|
||||
if (!empty($inviterIds)) {
|
||||
$inviters = Db::name('wechat_users')
|
||||
->where('id', 'in', $inviterIds)
|
||||
->field('id, nickname, phone')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($inviters as $iv) {
|
||||
$inviterMap[(int) $iv['id']] = $iv;
|
||||
}
|
||||
}
|
||||
foreach ($crRows as $cr) {
|
||||
$oid = (int) ($cr['orderId'] ?? 0);
|
||||
if ($oid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$iv = $inviterMap[(int) ($cr['inviterId'] ?? 0)] ?? null;
|
||||
$commissionsByOrder[$oid][] = [
|
||||
'id' => (int) $cr['id'],
|
||||
'inviterId' => (int) ($cr['inviterId'] ?? 0),
|
||||
'inviterName' => $iv['nickname'] ?? null,
|
||||
'inviterPhone' => $iv['phone'] ?? null,
|
||||
'commissionFen' => (int) ($cr['commissionFen'] ?? 0),
|
||||
'rate' => isset($cr['rate']) ? (float) $cr['rate'] : null,
|
||||
'status' => (string) ($cr['status'] ?? ''),
|
||||
'paidAt' => isset($cr['paidAt']) ? (int) $cr['paidAt'] : null,
|
||||
'createdAt' => isset($cr['createdAt']) ? (int) $cr['createdAt'] : null,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$commissionsByOrder = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$uid = (int) ($row['userId'] ?? 0);
|
||||
$u = $usersMap[$uid] ?? null;
|
||||
$row['userName'] = $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid);
|
||||
$row['userPhone'] = $u ? ($u['phone'] ?? '') : '';
|
||||
$row['testData'] = $testsByOrder[$row['id']] ?? [];
|
||||
$row['commissions'] = $commissionsByOrder[$row['id']] ?? [];
|
||||
}
|
||||
|
||||
return success([
|
||||
|
||||
@@ -2,12 +2,115 @@
|
||||
namespace app\controller\admin\concern;
|
||||
|
||||
use app\common\PdpDiscResultText;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 从测试记录数组中解析 MBTI / DISC / PDP / 人脸子类型(与 AppUser 逻辑一致)
|
||||
*/
|
||||
trait ExtractsTestResults
|
||||
{
|
||||
/**
|
||||
* 基于测试记录推算冷脸分值(0-100)
|
||||
* - 面相 emotionScore/emotionNeutrality/microExpression 作为主输入
|
||||
* - MBTI I/E 作为补充(I 更冷,E 更暖)
|
||||
* 返回 ['score'=>int, 'level'=>'cold|neutral|warm'];如无任何线索返回 null
|
||||
*/
|
||||
public function calcColdFace(array $tests): ?array
|
||||
{
|
||||
$score = null; // 0-100
|
||||
$mbtiType = '';
|
||||
|
||||
foreach ($tests as $t) {
|
||||
$type = strtolower($t['testType'] ?? '');
|
||||
$raw = $t['result'] ?? ($t['resultData'] ?? '');
|
||||
if (!is_string($raw) || $raw === '') {
|
||||
continue;
|
||||
}
|
||||
$dec = json_decode($raw, true);
|
||||
if (!is_array($dec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'face' && $score === null) {
|
||||
$face = is_array($dec['face'] ?? null) ? $dec['face'] : $dec;
|
||||
$emotion = null;
|
||||
foreach (['coldFaceScore', 'coldScore', 'emotionNeutrality', 'neutrality'] as $k) {
|
||||
if (isset($face[$k]) && is_numeric($face[$k])) {
|
||||
$emotion = (float) $face[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($emotion === null && isset($face['emotionScore']) && is_numeric($face['emotionScore'])) {
|
||||
// emotionScore 越高越暖 → 冷脸分取反
|
||||
$emotion = 100 - (float) $face['emotionScore'];
|
||||
}
|
||||
if ($emotion === null && isset($face['microExpression']) && is_array($face['microExpression'])) {
|
||||
$mx = $face['microExpression'];
|
||||
$happy = is_numeric($mx['happy'] ?? null) ? (float) $mx['happy'] : 0;
|
||||
$neutral = is_numeric($mx['neutral'] ?? null) ? (float) $mx['neutral'] : 0;
|
||||
$total = $happy + $neutral + 1;
|
||||
$emotion = ($neutral / $total) * 100;
|
||||
}
|
||||
if ($emotion !== null) {
|
||||
if ($emotion <= 1) {
|
||||
$emotion = $emotion * 100;
|
||||
}
|
||||
$score = max(0, min(100, (int) round($emotion)));
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'mbti' && $mbtiType === '') {
|
||||
foreach (['mbtiType', 'type', 'result'] as $k) {
|
||||
$s = $this->coerceResultLabel($dec[$k] ?? null);
|
||||
if ($s !== '') {
|
||||
$mbtiType = strtoupper($s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($score === null && $mbtiType !== '') {
|
||||
$score = strpos($mbtiType, 'I') === 0 ? 62 : 42;
|
||||
}
|
||||
if ($score === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$level = 'neutral';
|
||||
if ($score > 65) {
|
||||
$level = 'cold';
|
||||
} elseif ($score < 35) {
|
||||
$level = 'warm';
|
||||
}
|
||||
|
||||
return ['score' => (int) $score, 'level' => $level];
|
||||
}
|
||||
|
||||
/**
|
||||
* 写回 user_profile 的冷脸字段(存在才更新,否则跳过;失败静默)
|
||||
*/
|
||||
protected function writeColdFace(int $userId, ?int $enterpriseId, array $cold): void
|
||||
{
|
||||
if (!isset($cold['score'], $cold['level'])) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$query = Db::name('user_profile')->where('userId', $userId);
|
||||
if ($enterpriseId) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$query->update([
|
||||
'coldFaceScore' => (int) $cold['score'],
|
||||
'coldFaceLevel' => (string) $cold['level'],
|
||||
'coldFaceUpdatedAt' => time(),
|
||||
'updatedAt' => time(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 字段不存在(未执行迁移)时直接忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果字段可能为数组/对象,禁止直接 (string) 强转导致 Array to string conversion
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user