feat: 高考志愿与定价分销链路更新(管理端/API/小程序)

- 新增高考志愿核心服务、模型、控制器与迁移脚本

- 同步管理端定价/分销/设置与小程序入口、历史、测试选择和支付逻辑

- 补充相关开发文档

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-25 16:19:08 +08:00
parent 513093a5e0
commit c4ac0f8a08
56 changed files with 6100 additions and 68 deletions

View File

@@ -377,6 +377,8 @@ class AppUser extends BaseController
$testTypes = [];
$payStats = [];
$enterpriseNames = [];
$gaokaoMap = [];
$gaokaoReportMap = [];
if (!empty($ids)) {
$trBase = Db::name('test_results')->where('userId', 'in', $ids);
$trAggRows = (clone $trBase)
@@ -466,6 +468,45 @@ class AppUser extends BaseController
} catch (\Throwable $e) {
$payStats = [];
}
try {
$gqRows = Db::name('gaokao_user_profile')
->whereIn('userId', $ids)
->field('id,userId,tenantId,entryStatus,mbtiStatus,pdpStatus,discStatus,formStatus,analyzeStatus,lastAnalyzeAt,latestReportId')
->select()
->toArray();
$reportIds = [];
foreach ($gqRows as $gr) {
$uid = (int) ($gr['userId'] ?? 0);
if ($uid > 0) {
$gaokaoMap[$uid] = $gr;
if (!empty($gr['latestReportId'])) {
$reportIds[] = (int) $gr['latestReportId'];
}
}
}
$reportIds = array_values(array_unique(array_filter($reportIds)));
if ($reportIds) {
$rRows = Db::name('test_results')
->whereIn('id', $reportIds)
->where('testType', 'gaokao')
->field('id,resultData')
->select()
->toArray();
foreach ($rRows as $rr) {
$raw = $rr['resultData'] ?? '';
$rd = is_string($raw) ? (json_decode($raw, true) ?: []) : (is_array($raw) ? $raw : []);
$ov = (string) ($rd['overview'] ?? '');
if ($ov === '' && isset($rd['report']['overview'])) {
$ov = (string) $rd['report']['overview'];
}
$gaokaoReportMap[(int) $rr['id']] = $ov;
}
}
} catch (\Throwable $e) {
$gaokaoMap = [];
$gaokaoReportMap = [];
}
}
foreach ($list as &$row) {
@@ -493,6 +534,15 @@ class AppUser extends BaseController
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
$row['totalPaidAmount'] = $totalPaidFen;
$row['totalPaidAmountYuan'] = $totalPaidFen > 0 ? round($totalPaidFen / 100, 2) : 0;
$gq = $gaokaoMap[$id] ?? null;
$row['gaokaoEntryStatus'] = $gq ? (int) ($gq['entryStatus'] ?? 0) : 0;
$row['gaokaoAnalyzeStatus'] = $gq ? (int) ($gq['analyzeStatus'] ?? 0) : 0;
$row['gaokaoFormStatus'] = $gq ? (int) ($gq['formStatus'] ?? 0) : 0;
$row['gaokaoTenantId'] = $gq ? (int) ($gq['tenantId'] ?? 0) : 0;
$row['gaokaoLastAnalyzeAt'] = $gq ? (int) ($gq['lastAnalyzeAt'] ?? 0) : 0;
$rid = $gq ? (int) ($gq['latestReportId'] ?? 0) : 0;
$row['gaokaoOverview'] = $rid > 0 ? (string) ($gaokaoReportMap[$rid] ?? '') : '';
}
return paginate_response($list, $total, $page, $pageSize);
@@ -564,6 +614,33 @@ class AppUser extends BaseController
$data['resumeUploads'] = ResumeUploadsAdminService::listForWechatUser((int) $id, null);
try {
$gq = Db::name('gaokao_user_profile')->where('userId', (int) $id)->find();
if ($gq) {
$data['gaokaoProfile'] = $gq;
$rid = (int) ($gq['latestReportId'] ?? 0);
if ($rid > 0) {
$data['gaokaoLatestReport'] = Db::name('test_results')
->where('id', $rid)
->where('testType', 'gaokao')
->find();
}
$data['gaokaoOrders'] = Db::name('orders')
->where('userId', (int) $id)
->where('productType', 'gaokao')
->order('id', 'desc')
->limit(20)
->select()
->toArray();
} else {
$data['gaokaoProfile'] = null;
$data['gaokaoOrders'] = [];
}
} catch (\Throwable $e) {
$data['gaokaoProfile'] = null;
$data['gaokaoOrders'] = [];
}
return success($data);
}

View File

@@ -399,7 +399,7 @@ class Distribution extends BaseController
private static function defaultTestSettings(): array
{
$item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false];
return ['face' => $item, 'mbti' => $item, 'sbti' => $item, 'disc' => $item, 'pdp' => $item];
return ['face' => $item, 'mbti' => $item, 'sbti' => $item, 'disc' => $item, 'pdp' => $item, 'gaokao' => $item];
}
private static function sanitizeTestSettings($raw): array

View File

@@ -0,0 +1,105 @@
<?php
namespace app\controller\superadmin;
use app\BaseController;
use think\facade\Db;
use think\facade\Request;
/**
* 超管:高考用户管理(全平台)
*/
class GaokaoUser extends BaseController
{
private function authOk(): bool
{
$user = $this->request->user ?? null;
return !!($user && ($user['role'] ?? '') === 'superadmin');
}
public function index()
{
if (!$this->authOk()) {
return error('无权限访问', 403);
}
$page = max(1, (int) Request::param('page', 1));
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
$keyword = trim((string) Request::param('keyword', ''));
$tenantId = (int) Request::param('tenantId', 0);
$q = Db::name('gaokao_user_profile')->alias('g')
->join('wechat_users w', 'w.id = g.userId')
->leftJoin('enterprises e', 'e.id = g.tenantId');
if ($keyword !== '') {
$q->whereRaw('(w.nickname LIKE ? OR w.phone LIKE ? OR g.name LIKE ?)', ['%' . $keyword . '%', '%' . $keyword . '%', '%' . $keyword . '%']);
}
if ($tenantId > 0) {
$q->where('g.tenantId', $tenantId);
}
$total = (int) (clone $q)->count();
$rows = (clone $q)
->field('g.*,w.nickname,w.phone,w.avatar,e.name as tenantName')
->order('g.id', 'desc')
->page($page, $pageSize)
->select()
->toArray();
return paginate_response($rows, $total, $page, $pageSize);
}
public function detail($id)
{
if (!$this->authOk()) {
return error('无权限访问', 403);
}
$row = Db::name('gaokao_user_profile')->alias('g')
->join('wechat_users w', 'w.id = g.userId')
->leftJoin('enterprises e', 'e.id = g.tenantId')
->where('g.id', (int) $id)
->field('g.*,w.nickname,w.phone,w.avatar,e.name as tenantName')
->find();
if (!$row) {
return error('记录不存在', 404);
}
$uid = (int) $row['userId'];
$reports = Db::name('test_results')
->where('userId', $uid)
->where('testType', 'gaokao')
->order('id', 'desc')
->limit(20)
->select()
->toArray();
$orders = Db::name('orders')
->where('userId', $uid)
->where('productType', 'gaokao')
->order('id', 'desc')
->limit(20)
->select()
->toArray();
$orderIds = array_map(static function ($o) {
return (int) ($o['id'] ?? 0);
}, $orders);
$orderIds = array_values(array_filter($orderIds));
$commissions = [];
if ($orderIds !== []) {
$commissions = Db::name('commission_records')
->whereIn('orderId', $orderIds)
->order('id', 'desc')
->select()
->toArray();
}
$binding = Db::name('distribution_bindings')
->where('inviteeId', $uid)
->where('status', 'active')
->where('expireAt', '>', time())
->order('id', 'desc')
->find();
return success([
'profile' => $row,
'reports' => $reports,
'orders' => $orders,
'commissions' => $commissions,
'distributionBinding' => $binding,
]);
}
}

View File

@@ -42,10 +42,19 @@ class Pricing extends BaseController
if (!$config) {
return error('定价配置不存在', 404);
}
$cfg = $config->config;
if (is_array($cfg)) {
if ($type === 'personal') {
$cfg = self::normalizePersonalPricingConfig($cfg);
} elseif ($type === 'enterprise') {
$cfg = self::normalizeEnterprisePricingConfig($cfg);
}
}
return success([
'type' => $config->type,
'enterpriseId' => $config->enterpriseId,
'config' => $config->config
'config' => $cfg
]);
} else {
// 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表
@@ -60,6 +69,18 @@ class Pricing extends BaseController
}
}
}
if (is_array($result['personal'])) {
$result['personal'] = self::normalizePersonalPricingConfig($result['personal']);
}
if (is_array($result['enterprise'])) {
$result['enterprise'] = self::normalizeEnterprisePricingConfig($result['enterprise']);
}
foreach ($result['enterpriseList'] as $i => $entRow) {
if (isset($entRow['config']) && is_array($entRow['config'])) {
$result['enterpriseList'][$i]['config'] = self::normalizeEnterprisePricingConfig($entRow['config']);
}
}
return success($result);
}
}
@@ -196,5 +217,44 @@ class Pricing extends BaseController
return success(null, "成功保存 {$successCount} 个配置");
}
/**
* 旧库 JSON 可能缺少高考等字段,合并默认值便于管理端展示与保存
*
* @param array<string,mixed> $cfg
* @return array<string,mixed>
*/
private static function normalizePersonalPricingConfig(array $cfg): array
{
$defaults = [
'face' => 0,
'mbti' => 0,
'disc' => 0,
'pdp' => 0,
'sbti' => 0,
'gaokao' => 0,
];
return array_merge($defaults, $cfg);
}
/**
* @param array<string,mixed> $cfg
* @return array<string,mixed>
*/
private static function normalizeEnterprisePricingConfig(array $cfg): array
{
$defaults = [
'face' => 0,
'mbti' => 0,
'pdp' => 0,
'disc' => 0,
'sbti' => 0,
'gaokao' => 0,
'minRecharge' => 0,
];
return array_merge($defaults, $cfg);
}
}