diff --git a/Server/application/store/config/route.php b/Server/application/store/config/route.php index c5fcfe0f..ae09c5df 100644 --- a/Server/application/store/config/route.php +++ b/Server/application/store/config/route.php @@ -35,9 +35,8 @@ Route::group('v1/store', function () { // 数据统计相关路由 Route::group('statistics', function () { Route::get('overview', 'app\store\controller\StatisticsController@getOverview'); // 获取数据概览 - Route::get('customer-analysis', 'app\store\controller\StatisticsController@getCustomerAnalysis'); // 获取客户分析数据 - Route::get('interaction-analysis', 'app\store\controller\StatisticsController@getInteractionAnalysis'); // 获取互动分析数据 - }); + Route::get('comprehensive-analysis', 'app\store\controller\StatisticsController@getComprehensiveAnalysis'); // 获取综合分析数据 + }); // 供应商相关路由 Route::group('vendor', function () { diff --git a/Server/application/store/controller/StatisticsController.php b/Server/application/store/controller/StatisticsController.php index 2096b2bc..17c9226c 100644 --- a/Server/application/store/controller/StatisticsController.php +++ b/Server/application/store/controller/StatisticsController.php @@ -4,6 +4,7 @@ namespace app\store\controller; use app\store\model\WechatFriendModel; use app\store\model\WechatMessageModel; +use app\store\model\TrafficOrderModel; use think\Db; @@ -18,8 +19,8 @@ class StatisticsController extends BaseController public function getOverview() { try { - $companyId = $this->userInfo['companyId']; - $userId = $this->userInfo['id']; + $companyId = $this->userInfo['companyId']; + $userId = $this->userInfo['id']; // 构建查询条件 $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); @@ -34,7 +35,7 @@ class StatisticsController extends BaseController ->value('wechatId'); } - $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId',$ownerWechatIds)->column('id'); + $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id'); // 获取时间范围 @@ -45,43 +46,57 @@ class StatisticsController extends BaseController $lastEndTime = $timeRange['last_end_time']; - - - // 1. 总客户数 - $totalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds) - ->where('isDeleted',0) - ->count(); + $totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->whereTime('createTime', '>=', $startTime) + ->whereTime('createTime', '<', $endTime) + ->count(); // 上期总客户数 - $lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)->count(); + $lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $lastStartTime) + ->whereTime('createTime', '<', $lastEndTime) + ->count(); // 2. 新增客户数 - $newCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds) - ->whereTime('createTime', '>=', $startTime) - ->whereTime('createTime', '<', $endTime) - ->count(); + $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $startTime) + ->whereTime('createTime', '<', $endTime) + ->count(); // 上期新增客户数 - $lastNewCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds) - ->whereTime('createTime', '>=', $lastStartTime) - ->whereTime('createTime', '<', $lastEndTime) - ->count(); + $lastNewCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $lastStartTime) + ->whereTime('createTime', '<', $lastEndTime) + ->count(); //3. 互动次数 $interactionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->count(); + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->count(); // 上期互动次数 $lastInteractionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) - ->where('createTime', '>=', $lastStartTime) - ->where('createTime', '<', $lastEndTime) - ->count(); - + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->count(); - + // 4. RFM 平均值计算(不查询上期数据) + $rfmStats = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->field('AVG(`R`) as avgR, AVG(`F`) as avgF, AVG(`M`) as avgM') + ->find(); + + // 处理查询结果,如果字段为null则默认为0 + $avgR = isset($rfmStats['avgR']) && $rfmStats['avgR'] !== null ? round((float)$rfmStats['avgR'], 2) : 0; + $avgF = isset($rfmStats['avgF']) && $rfmStats['avgF'] !== null ? round((float)$rfmStats['avgF'], 2) : 0; + $avgM = isset($rfmStats['avgM']) && $rfmStats['avgM'] !== null ? round((float)$rfmStats['avgM'], 2) : 0; + + // 计算三者的平均值 + $avgRFM = ($avgR + $avgF + $avgM) / 3; + $avgRFM = round($avgRFM, 2); // 计算环比增长率 $customerGrowth = $this->calculateGrowth($totalCustomers, $lastTotalCustomers); @@ -99,6 +114,16 @@ class StatisticsController extends BaseController 'interaction_count' => [ 'value' => $interactionCount, 'growth' => $interactionGrowth + ], + 'conversion_rate' => [ + 'value' => 10, + 'growth' => 15 + ], + 'account_value' => [ + 'avg_r' => $avgR, + 'avg_f' => $avgF, + 'avg_m' => $avgM, + 'avg_rfm' => $avgRFM ] ]; @@ -108,14 +133,15 @@ class StatisticsController extends BaseController } } + /** - * 获取客户分析数据 + * 获取综合分析数据 */ - public function getCustomerAnalysis() + public function getComprehensiveAnalysis() { try { - $companyId = $this->userInfo['companyId']; - $userId = $this->userInfo['id']; + $companyId = $this->userInfo['companyId']; + $userId = $this->userInfo['id']; // 构建查询条件 $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); @@ -129,58 +155,189 @@ class StatisticsController extends BaseController ->order('id DESC') ->value('wechatId'); } + $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id'); - - // 获取时间范围 $timeRange = $this->getTimeRange(); $startTime = $timeRange['start_time']; $endTime = $timeRange['end_time']; + $lastStartTime = $timeRange['last_start_time']; + $lastEndTime = $timeRange['last_end_time']; - // 1. 客户增长趋势数据 - $totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->where('isDeleted',0) - ->whereTime('createTime', '<', $endTime) + // ========== 1. 客户平均转化金额 ========== + // 获取有订单的客户数(去重) + $convertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('identifier') + ->column('identifier'); + $convertedCustomerCount = count($convertedCustomers); + + // 总销售额 + $totalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->sum('actualPay'); + $totalSales = $totalSales ?: 0; + + // 客户平均转化金额 + $avgConversionAmount = $convertedCustomerCount > 0 ? round($totalSales / $convertedCustomerCount, 2) : 0; + + // ========== 2. 价值指标 ========== + // 销售总额(已计算) + + // 平均订单金额(总订单数) + $totalOrderCount = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) ->count(); + $avgOrderAmount = $totalOrderCount > 0 ? round($totalSales / $totalOrderCount, 2) : 0; + + // 高价值客户(消费超过平均订单金额的客户) + // 先获取每个客户的消费总额 + $customerTotalSpend = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('identifier, SUM(actualPay) as totalSpend') + ->group('identifier') + ->select(); + + $highValueCustomerCount = 0; + $avgCustomerSpend = $convertedCustomerCount > 0 ? ($totalSales / $convertedCustomerCount) : 0; + foreach ($customerTotalSpend as $customer) { + if ($customer['totalSpend'] > $avgCustomerSpend) { + $highValueCustomerCount++; + } + } + + // 高价值客户百分比 + $totalCustomersForCalc = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->count(); + $highValueCustomerPercent = $totalCustomersForCalc > 0 ? round(($highValueCustomerCount / $totalCustomersForCalc) * 100, 1) : 0; + // ========== 3. 增长趋势 ========== + // 上期销售额 + $lastTotalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->sum('actualPay'); + $lastTotalSales = $lastTotalSales ?: 0; + + // 周收益增长(金额差值) + $weeklyRevenueGrowth = round($totalSales - $lastTotalSales, 2); + + // 新客转化(新客户中有订单的人数) $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->where('isDeleted',0) - ->whereTime('createTime', '>=', $startTime) - ->whereTime('createTime', '<', $endTime) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->column('wechatId'); + + // 获取新客户中有订单的(identifier 对应 wechatId) + $newConvertedCustomers = 0; + if (!empty($newCustomers)) { + $newConvertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->whereIn('identifier', $newCustomers) + ->group('identifier') + ->count(); + } + + // 活跃客户增长(有互动的客户) + $activeCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('wechatFriendId') + ->count(); + + $lastActiveCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->group('wechatFriendId') + ->count(); + + // 活跃客户增长(人数差值) + $activeCustomerGrowth = $activeCustomers - $lastActiveCustomers; + + // ========== 4. 客户活跃度 ========== + // 按天统计每个客户的互动次数,然后分类 + // 高频互动用户数(平均每天3次以上) + $days = max(1, ($endTime - $startTime) / 86400); // 计算天数 + $highFrequencyThreshold = $days * 3; // 高频阈值 + + $highFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count > ' . $highFrequencyThreshold) ->count(); - // 计算流失客户数 - $lostCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->where('isDeleted',1) - ->where('createTime', '>', 0) - ->whereTime('deleteTime', '>=', $startTime) - ->whereTime('deleteTime', '<', $endTime) + // 中频互动用户数(平均每天1-3次) + $midFrequencyThreshold = $days * 1; + $midFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count >= ' . $midFrequencyThreshold . ' AND count <= ' . $highFrequencyThreshold) ->count(); - // 2. 客户来源分布数据 - // 朋友推荐 - $friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->whereIn('addFrom', [17, 1000017]) + // 低频互动用户数(少于平均每天1次) + $lowFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count < ' . $midFrequencyThreshold) ->count(); - // 微信搜索 - $wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->whereIn('addFrom', [3, 15, 1000003, 1000015]) - ->count(); + $frequency_analysis = [ + ['name' => '高频', 'value' => $highFrequencyUsers], + ['name' => '中频', 'value' => $midFrequencyUsers], + ['name' => '低频', 'value' => $lowFrequencyUsers] + ]; - // 微信群 - $wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) - ->whereIn('addFrom', [14, 1000014]) - ->count(); + // ========== 5. 转化客户来源 ========== + // 只统计有订单的客户来源(identifier 对应 wechatId) + $convertedFriendIds = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('identifier') + ->column('identifier'); + + $friendRecommend = 0; + $wechatSearch = 0; + $wechatGroup = 0; + + if (!empty($convertedFriendIds)) { + // 朋友推荐(有订单的) + $friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [17, 1000017]) + ->count(); - // 其他渠道(总数减去已知渠道) - $otherSource = $totalCustomers - $friendRecommend - $wechatSearch - $wechatGroup; - $otherSource = max(0, $otherSource); // 确保不会出现负数 + // 微信搜索(有订单的) + $wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [3, 15, 1000003, 1000015]) + ->count(); + + // 微信群(有订单的) + $wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [14, 1000014]) + ->count(); + } + + $totalConvertedCustomers = $convertedCustomerCount; + $otherSource = max(0, $totalConvertedCustomers - $friendRecommend - $wechatSearch - $wechatGroup); // 计算百分比 - $calculatePercentage = function($value) use ($totalCustomers) { - if ($totalCustomers <= 0) return 0; - return round(($value / $totalCustomers) * 100, 2); + $calculatePercentage = function ($value) use ($totalConvertedCustomers) { + if ($totalConvertedCustomers <= 0) return 0; + return round(($value / $totalConvertedCustomers) * 100, 2); }; $sourceDistribution = [ @@ -198,151 +355,24 @@ class StatisticsController extends BaseController 'name' => '微信群', 'value' => $calculatePercentage($wechatGroup) . '%', 'count' => $wechatGroup - ], - [ - 'name' => '其他渠道', - 'value' => $calculatePercentage($otherSource) . '%', - 'count' => $otherSource ] ]; - $data = [ - 'trend' => [ - 'total' => $totalCustomers, - 'new' => $newCustomers, - 'lost' => $lostCustomers - ], - 'source_distribution' => $sourceDistribution - ]; - - return successJson($data); - } catch (\Exception $e) { - return errorJson('获取客户分析数据失败:' . $e->getMessage()); - } - } - - /** - * 获取互动分析数据 - */ - public function getInteractionAnalysis() - { - try { - $companyId = $this->userInfo['companyId']; - $userId = $this->userInfo['id']; - - // 构建查询条件 - $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); - if (empty($deviceIds)) { - return errorJson('设备不存在'); - } - $ownerWechatIds = []; - foreach ($deviceIds as $deviceId) { - $ownerWechatIds[] = Db::name('device_wechat_login') - ->where(['deviceId' => $deviceId]) - ->order('id DESC') - ->value('wechatId'); - } - $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId',$ownerWechatIds)->column('id'); - - // 获取时间范围 - $timeRange = $this->getTimeRange(); - $startTime = $timeRange['start_time']; - $endTime = $timeRange['end_time']; - - // 不再需要转换为时间戳,因为getTimeRange已经转换 - // $startTimestamp = strtotime($startTime); - // $endTimestamp = strtotime($endTime); - - // 1. 互动频率分析 - // 高频互动用户数(每天3次以上) - $highFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId' , $wechatAccountIds) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->field('wechatFriendId, COUNT(*) as count') - ->group('wechatFriendId') - ->having('count > 3') - ->count(); - - // 中频互动用户数(每天1-3次) - $midFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId' , $wechatAccountIds) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->field('wechatFriendId, COUNT(*) as count') - ->group('wechatFriendId') - ->having('count >= 1 AND count <= 3') - ->count(); - - // 低频互动用户数(仅有1次) - $lowFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId' , $wechatAccountIds) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->field('wechatFriendId, COUNT(*) as count') - ->group('wechatFriendId') - ->having('count = 1') - ->count(); - - // 2. 互动内容分析 - // 文字消息数量 - $textMessages = WechatMessageModel::where([ - 'msgType' => 1 - ]) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->whereIn('wechatAccountId' , $wechatAccountIds) - ->count(); - - // 图片互动数量 - $imgInteractions = WechatMessageModel::where([ - 'msgType' => 3 - ]) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->whereIn('wechatAccountId' , $wechatAccountIds) - ->count(); - - // 群聊互动数量 - $groupInteractions = WechatMessageModel::where([ - 'type' => 2 - ]) - ->where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->whereIn('wechatAccountId' , $wechatAccountIds) - ->count(); - - // 产品咨询数量 (通过消息内容模糊查询) - $productInquiries = WechatMessageModel::where('createTime', '>=', $startTime) - ->where('createTime', '<', $endTime) - ->where('content', 'like', '%产品%') - ->whereOr('content', 'like', '%价格%') - ->whereOr('content', 'like', '%购买%') - ->whereOr('content', 'like', '%优惠%') - ->whereIn('wechatAccountId' , $wechatAccountIds) - ->count(); - // 构建返回数据 $data = [ - 'frequency_analysis' => [ - 'high_frequency' => $highFrequencyUsers, - 'mid_frequency' => $midFrequencyUsers, - 'low_frequency' => $lowFrequencyUsers, - 'chart_data' => [ - ['name' => '高频互动', 'value' => $highFrequencyUsers], - ['name' => '中频互动', 'value' => $midFrequencyUsers], - ['name' => '低频互动', 'value' => $lowFrequencyUsers] - ] + 'avg_conversion_amount' => $avgConversionAmount, // 客户平均转化金额 + 'value_indicators' => [ + 'total_sales' => round($totalSales, 2), // 销售总额 + 'avg_order_amount' => $avgOrderAmount, // 平均订单金额 + 'high_value_customers' => $highValueCustomerPercent . '%' // 高价值客户 ], - 'content_analysis' => [ - 'text_messages' => $textMessages, - 'img_interactions' => $imgInteractions, - 'group_interactions' => $groupInteractions, - 'product_inquiries' => $productInquiries, - 'chart_data' => [ - ['name' => '文字互动', 'value' => $textMessages], - ['name' => '图片互动', 'value' => $imgInteractions], - ['name' => '群聊互动', 'value' => $groupInteractions], - ['name' => '产品咨询', 'value' => $productInquiries] - ] - ] + 'growth_trend' => [ + 'weekly_revenue_growth' => $weeklyRevenueGrowth, // 周收益增长(金额) + 'new_customer_conversion' => $newConvertedCustomers, // 新客转化(人数) + 'active_customer_growth' => $activeCustomerGrowth // 活跃客户增长(人数差值) + ], + 'frequency_analysis' => $frequency_analysis, // 客户活跃度 + 'source_distribution' => $sourceDistribution // 转化客户来源 ]; return successJson($data); @@ -353,7 +383,7 @@ class StatisticsController extends BaseController /** * 获取时间范围 - * + * * @param bool $toTimestamp 是否将日期转为时间戳,默认为true * @return array 时间范围数组 */ @@ -361,7 +391,7 @@ class StatisticsController extends BaseController { // 可选:today, yesterday, this_week, last_week, this_month, this_quarter, this_year $timeType = input('time_type', 'this_week'); - + switch ($timeType) { case 'today': // 今日 $startTime = date('Y-m-d'); @@ -369,35 +399,35 @@ class StatisticsController extends BaseController $lastStartTime = date('Y-m-d', strtotime('-1 day')); // 昨日 $lastEndTime = $startTime; break; - + case 'yesterday': // 昨日 $startTime = date('Y-m-d', strtotime('-1 day')); $endTime = date('Y-m-d'); $lastStartTime = date('Y-m-d', strtotime('-2 day')); // 前日 $lastEndTime = $startTime; break; - + case 'this_week': // 本周 $startTime = date('Y-m-d', strtotime('monday this week')); $endTime = date('Y-m-d', strtotime('monday next week')); $lastStartTime = date('Y-m-d', strtotime('monday last week')); // 上周一 $lastEndTime = $startTime; break; - + case 'last_week': // 上周 $startTime = date('Y-m-d', strtotime('monday last week')); $endTime = date('Y-m-d', strtotime('monday this week')); $lastStartTime = date('Y-m-d', strtotime('monday last week', strtotime('last week'))); // 上上周一 $lastEndTime = $startTime; break; - + case 'this_month': // 本月 $startTime = date('Y-m-01'); $endTime = date('Y-m-d', strtotime(date('Y-m-01') . ' +1 month')); $lastStartTime = date('Y-m-01', strtotime('-1 month')); // 上月初 $lastEndTime = $startTime; break; - + case 'this_quarter': // 本季度 $month = date('n'); $quarter = ceil($month / 3); @@ -408,14 +438,14 @@ class StatisticsController extends BaseController $lastStartTime = date('Y-m-d', strtotime($startTime . ' -3 month')); $lastEndTime = $startTime; break; - + case 'this_year': // 本年度 $startTime = date('Y-01-01'); $endTime = (date('Y') + 1) . '-01-01'; $lastStartTime = (date('Y') - 1) . '-01-01'; // 去年初 $lastEndTime = $startTime; break; - + default: $startTime = date('Y-m-d', strtotime('monday this week')); $endTime = date('Y-m-d', strtotime('monday next week')); diff --git a/Server/application/store/model/TrafficOrderModel.php b/Server/application/store/model/TrafficOrderModel.php new file mode 100644 index 00000000..4d51d66f --- /dev/null +++ b/Server/application/store/model/TrafficOrderModel.php @@ -0,0 +1,11 @@ + import { hasValidToken, redirectToLogin } from './api/utils/auth'; - import { appApi } from './api/modules/app'; - import UpdateModal from './components/UpdateModal.vue'; export default { - components: { - UpdateModal - }, - data() { - return { - // #ifdef APP-PLUS - // 更新弹窗相关数据(仅APP端) - showUpdateModal: false, - updateInfo: { - version: '', - updateContent: '', - downloadUrl: '', - forceUpdate: false - } - // #endif - }; - }, onLaunch: function() { console.log('App Launch'); - // 检测APP更新 - this.checkAppUpdate(); // 全局检查token this.checkToken(); }, onShow: function() { console.log('App Show'); - // 每次显示时检测APP更新 - this.checkAppUpdate(); // 应用恢复时再次检查token this.checkToken(); }, @@ -44,69 +21,12 @@ // 获取当前页面 const pages = getCurrentPages(); const currentPage = pages.length ? pages[pages.length - 1] : null; - const currentRoute = currentPage ? currentPage.route : ''; // 如果token无效且不在登录页面,则跳转到登录页面 - if (!hasValidToken() && currentRoute && currentRoute !== 'pages/login/index') { - console.log('Token无效,从', currentRoute, '重定向到登录页面'); + if (!hasValidToken() && currentPage && currentPage.route !== 'pages/login/index') { redirectToLogin(); } - }, - - // 检测APP更新 - async checkAppUpdate() { - // #ifdef APP-PLUS - try { - console.log('开始检测APP更新...'); - const res = await appApi.checkUpdate(); - console.log('更新检测结果:', res); - - if (res.code === 200 && res.data) { - const data = res.data; - - // 清理 downloadUrl 中的换行符 - const downloadUrl = data.downloadUrl ? data.downloadUrl.trim() : ''; - - // 设置更新信息 - this.updateInfo = { - version: data.version || '', - updateContent: data.updateContent || '本次更新包含性能优化和问题修复', - downloadUrl: downloadUrl, - forceUpdate: data.forceUpdate || false - }; - - // 显示更新弹窗 - this.showUpdateModal = true; - } - } catch (error) { - console.error('检测更新失败:', error); - // 更新检测失败不影响应用正常使用,只记录日志 } - // #endif - }, - - // 处理更新确认 - handleUpdateConfirm(downloadUrl) { - // #ifdef APP-PLUS - if (downloadUrl) { - plus.runtime.openURL(downloadUrl); - } - // #endif - this.showUpdateModal = false; - }, - - // 处理更新取消 - handleUpdateCancel() { - if (this.updateInfo.forceUpdate) { - // 强制更新时,取消则退出应用 - // #ifdef APP-PLUS - plus.runtime.quit(); - // #endif - } else { - // 关闭弹窗 - this.showUpdateModal = false; - } - } } } @@ -139,21 +59,6 @@ - - \ No newline at end of file diff --git a/Store_vue/components/SideMenu.vue b/Store_vue/components/SideMenu.vue index 1ac5835a..33eb8d36 100644 --- a/Store_vue/components/SideMenu.vue +++ b/Store_vue/components/SideMenu.vue @@ -1,7 +1,61 @@ @@ -193,10 +240,8 @@ import LoginRegister from './LoginRegister.vue'; import DataStatistics from './DataStatistics.vue'; import CustomerManagement from './CustomerManagement.vue'; - import UpdateDialog from './UpdateDialog.vue'; import { hasValidToken, clearToken, redirectToLogin } from '../api/utils/auth'; import { request, APP_CONFIG } from '../api/config'; - import { appApi } from '../api/modules/app'; export default { name: "SideMenu", @@ -206,8 +251,7 @@ SystemSettings, LoginRegister, DataStatistics, - CustomerManagement, - UpdateDialog + CustomerManagement }, props: { show: { @@ -233,19 +277,22 @@ showLoginPageFlag: false, isLoggedIn: false, // 用户登录状态 userInfo: null, // 用户信息 - // 版本信息 - currentVersion: APP_CONFIG.version, // 当前版本 + // 版本更新相关 + currentVersion: '', // 当前版本 latestVersion: '', // 最新版本 hasNewVersion: false, // 是否有新版本 checkingUpdate: false, // 是否正在检查更新 - // 更新弹窗 - showUpdateDialog: false, + showUpdateDialog: false, // 是否显示更新弹窗 updateInfo: { - version: '', - updateContent: '', - downloadUrl: '', - forceUpdate: false - } + version: '', // 新版本号 + updateContent: [], // 更新内容列表 + downloadUrl: '', // 下载地址 + forceUpdate: false // 是否强制更新 + }, + // 下载相关 + downloading: false, // 是否正在下载 + downloadProgress: 0, // 下载进度 0-100 + downloadTask: null // 下载任务对象 } }, watch: { @@ -267,6 +314,8 @@ this.checkLoginStatus(); // 获取功能开关状态 this.getFunctionStatus(); + // 获取当前版本号并自动检查更新 + this.getCurrentVersionAndCheckUpdate(); }, methods: { // 检查登录状态 @@ -497,36 +546,75 @@ this.showSystemSettingsPage = true; }, - // 版本号比较函数 - // 返回值: 1表示v1>v2, -1表示v1 { + this.currentVersion = info.version || '1.0.0'; + }); + // #endif - for (let i = 0; i < maxLen; i++) { - const num1 = arr1[i] || 0; - const num2 = arr2[i] || 0; - - if (num1 > num2) return 1; - if (num1 < num2) return -1; - } + // #ifndef APP-PLUS + this.currentVersion = '1.0.0'; + // #endif + }, + + // 获取当前版本号并自动检查更新 + getCurrentVersionAndCheckUpdate() { + // #ifdef APP-PLUS + plus.runtime.getProperty(plus.runtime.appid, (info) => { + this.currentVersion = info.version || '1.0.0'; + console.log('获取到当前版本号:', this.currentVersion); + // 版本号获取完成后,自动检查更新(延迟500ms,确保应用已完全启动) + setTimeout(() => { + this.handleCheckUpdate(true); // 传入 true 表示自动检查,不显示"已是最新版本"提示 + }, 500); + }); + // #endif - return 0; + // #ifndef APP-PLUS + this.currentVersion = '1.0.0'; + // #endif }, // 检查更新 - async handleCheckUpdate() { + // autoCheck: true 表示自动检查(应用启动时),不显示"已是最新版本"提示 + // autoCheck: false 表示手动检查(用户点击按钮),显示所有提示 + async handleCheckUpdate(autoCheck = false) { // #ifdef APP-PLUS if (this.checkingUpdate) { return; // 正在检查中,避免重复请求 } + // 如果版本号还没获取到,先获取版本号 + if (!this.currentVersion) { + // #ifdef APP-PLUS + plus.runtime.getProperty(plus.runtime.appid, (info) => { + this.currentVersion = info.version || '1.0.0'; + // 版本号获取完成后,继续检查更新 + setTimeout(() => { + this.handleCheckUpdate(autoCheck); + }, 100); + }); + // #endif + return; + } + this.checkingUpdate = true; try { console.log('开始检查更新,当前版本:', this.currentVersion); - const res = await appApi.checkUpdate(); + + // 调用检查更新接口 + const res = await request({ + url: '/v1/app/update', + method: 'GET', + data: { + version: this.currentVersion, + type: 'aiStore' + } + }); + console.log('更新检测结果:', res); if (res.code === 200 && res.data) { @@ -537,42 +625,286 @@ const compareResult = this.compareVersion(this.latestVersion, this.currentVersion); if (compareResult > 0) { - // 线上版本大于本地版本 + // 线上版本大于本地版本,有新版本 this.hasNewVersion = true; - // 设置更新信息并显示自定义弹窗 + // 设置更新信息 this.updateInfo = { version: data.version || '', - updateContent: data.updateContent || '', - downloadUrl: data.downloadUrl ? data.downloadUrl.trim() : '', + updateContent: this.parseUpdateContent(data.updateContent || data.content || ''), + downloadUrl: data.downloadUrl || data.url || '', forceUpdate: data.forceUpdate || false }; - this.showUpdateDialog = true; + + // 根据检查类型决定是否显示弹窗 + // autoCheck === false 表示手动检查,autoCheck === true 表示自动检查 + if (autoCheck === false) { + // 手动检查:有新版本时总是显示弹窗(不受每日限制) + console.log('手动检查更新,直接显示弹窗'); + this.showUpdateDialog = true; + } else { + // 自动检查:每天只能弹出一次 + console.log('自动检查更新,检查今日是否已显示过弹窗'); + if (this.shouldShowUpdateDialog()) { + this.showUpdateDialog = true; + this.recordUpdateDialogShown(); + } else { + console.log('今天已显示过更新弹窗,不再自动弹出'); + } + } } else { // 已是最新版本 this.hasNewVersion = false; + // 只有手动检查时才显示"已是最新版本"提示 + if (!autoCheck) { + uni.showToast({ + title: '已是最新版本', + icon: 'success', + duration: 2000 + }); + } + } + } else { + // 只有手动检查时才显示错误提示 + if (!autoCheck) { uni.showToast({ - title: '已是最新版本', - icon: 'success' + title: res.msg || '检查更新失败', + icon: 'none', + duration: 2000 }); } } } catch (error) { console.error('检查更新失败:', error); - uni.showToast({ - title: '检查更新失败', - icon: 'none' - }); + // 只有手动检查时才显示错误提示 + if (!autoCheck) { + uni.showToast({ + title: '检查更新失败,请稍后重试', + icon: 'none', + duration: 2000 + }); + } } finally { this.checkingUpdate = false; } // #endif + + // #ifndef APP-PLUS + if (!autoCheck) { + uni.showToast({ + title: '此功能仅在APP中可用', + icon: 'none', + duration: 2000 + }); + } + // #endif + }, + + // 比较版本号,返回 1 表示 version1 > version2,返回 -1 表示 version1 < version2,返回 0 表示相等 + compareVersion(version1, version2) { + if (!version1 || !version2) return 0; + + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + const maxLength = Math.max(v1Parts.length, v2Parts.length); + + for (let i = 0; i < maxLength; i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part > v2Part) return 1; + if (v1Part < v2Part) return -1; + } + + return 0; + }, + + // 解析更新内容,将字符串转换为数组 + parseUpdateContent(content) { + if (!content) return []; + + // 如果已经是数组,直接返回 + if (Array.isArray(content)) { + return content; + } + + // 如果是字符串,尝试按换行符或分号分割 + if (typeof content === 'string') { + // 先尝试按换行符分割 + let items = content.split(/\n+/).filter(item => item.trim()); + + // 如果没有换行,尝试按分号分割 + if (items.length === 1) { + items = content.split(/[;;]/).filter(item => item.trim()); + } + + // 清理每个项目,移除可能的编号前缀(如 "1. ", "1、", "- " 等) + return items.map(item => { + return item.replace(/^[\d一二三四五六七八九十]+[\.、\s\-]*/, '').trim(); + }).filter(item => item); + } + + return []; + }, + + // 检查今天是否应该显示更新弹窗(用于自动检查) + shouldShowUpdateDialog() { + try { + const lastShownDate = uni.getStorageSync('updateDialogLastShownDate'); + if (!lastShownDate) { + return true; // 从未显示过,可以显示 + } + + // 获取今天的日期字符串(格式:YYYY-MM-DD) + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + + // 如果今天已经显示过,则不显示 + return lastShownDate !== todayStr; + } catch (e) { + console.error('检查更新弹窗显示状态失败:', e); + return true; // 出错时默认允许显示 + } + }, + + // 记录今天已显示更新弹窗 + recordUpdateDialogShown() { + try { + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + uni.setStorageSync('updateDialogLastShownDate', todayStr); + console.log('已记录更新弹窗显示日期:', todayStr); + } catch (e) { + console.error('记录更新弹窗显示日期失败:', e); + } }, // 关闭更新弹窗 closeUpdateDialog() { - this.showUpdateDialog = false; + if (!this.updateInfo.forceUpdate) { + this.showUpdateDialog = false; + } else { + uni.showToast({ + title: '此版本为重要更新,请升级后使用', + icon: 'none', + duration: 2000 + }); + } + }, + + // 开始下载更新 + startDownload() { + // #ifdef APP-PLUS + if (!this.updateInfo.downloadUrl) { + uni.showToast({ + title: '下载地址无效', + icon: 'none', + duration: 2000 + }); + return; + } + + if (this.downloading) { + return; // 已经在下载中 + } + + this.downloading = true; + this.downloadProgress = 0; + + // 创建下载任务 + const downloadPath = '_downloads/update_' + Date.now() + '.apk'; + this.downloadTask = plus.downloader.createDownload(this.updateInfo.downloadUrl, { + filename: downloadPath // 下载文件名 + }, (download, status) => { + if (status === 200) { + // 下载成功 + console.log('下载成功:', download.filename); + this.downloading = false; + this.downloadProgress = 100; + + // 安装APK + setTimeout(() => { + this.installAPK(download.filename); + }, 500); + } else { + // 下载失败 + console.error('下载失败:', status); + this.downloading = false; + this.downloadProgress = 0; + uni.showToast({ + title: '下载失败,请稍后重试', + icon: 'none', + duration: 2000 + }); + } + }); + + // 监听下载进度 + this.downloadTask.addEventListener('statechanged', (download, status) => { + switch (download.state) { + case 1: // 开始下载 + console.log('开始下载...'); + break; + case 2: // 连接到服务器 + console.log('连接到服务器...'); + break; + case 3: // 下载中 + if (download.totalSize > 0) { + const progress = Math.floor((download.downloadedSize / download.totalSize) * 100); + this.downloadProgress = Math.min(progress, 99); // 最大99,完成时再设为100 + console.log('下载进度:', this.downloadProgress + '%', '已下载:', download.downloadedSize, '总大小:', download.totalSize); + } + break; + case 4: // 下载完成 + console.log('下载完成'); + this.downloadProgress = 100; + break; + } + }); + + // 开始下载 + this.downloadTask.start(); + // #endif + }, + + // 安装APK + installAPK(filePath) { + // #ifdef APP-PLUS + try { + // 获取文件的完整路径 + const fullPath = plus.io.convertLocalFileSystemURL(filePath); + console.log('准备安装APK:', fullPath); + + plus.runtime.install(fullPath, {}, () => { + console.log('安装成功'); + uni.showToast({ + title: '安装成功', + icon: 'success', + duration: 1500 + }); + // 关闭弹窗 + setTimeout(() => { + this.showUpdateDialog = false; + }, 1500); + }, (error) => { + console.error('安装失败:', error); + uni.showToast({ + title: '安装失败,请到下载文件夹手动安装', + icon: 'none', + duration: 3000 + }); + }); + } catch (error) { + console.error('安装异常:', error); + uni.showToast({ + title: '安装异常,请到下载文件夹手动安装', + icon: 'none', + duration: 3000 + }); + } + // #endif } + } } @@ -858,4 +1190,178 @@ font-size: 12px; color: #999; } + + /* 更新弹窗样式 */ + .update-dialog-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10000; + display: flex; + justify-content: center; + align-items: center; + } + + .update-dialog { + position: relative; + width: 85%; + max-width: 600px; + background-color: #fff; + border-radius: 20px; + padding: 40px 30px 30px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + } + + /* 手机上最大宽度80% */ + @media screen and (max-width: 768px) { + .update-dialog { + width: 80%; + max-width: 80%; + } + } + + .update-rocket { + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, #5096ff 0%, #6b7fff 100%); + border-radius: 50%; + box-shadow: 0 4px 15px rgba(80, 150, 255, 0.3); + } + + .update-rocket text { + font-size: 50px; + line-height: 1; + } + + .update-version-info { + text-align: center; + margin-top: 20px; + margin-bottom: 25px; + } + + .update-version-text { + font-size: 18px; + font-weight: 600; + color: #333; + } + + .update-content-list { + margin-bottom: 30px; + max-height: 300px; + overflow-y: auto; + } + + .update-content-item { + display: flex; + align-items: flex-start; + margin-bottom: 12px; + padding: 0 5px; + } + + .update-item-number { + font-size: 15px; + color: #5096ff; + font-weight: 600; + margin-right: 8px; + min-width: 20px; + } + + .update-item-text { + font-size: 15px; + color: #666; + line-height: 1.6; + flex: 1; + } + + .download-progress-wrapper { + margin-bottom: 20px; + padding: 0 5px; + } + + .download-progress-bar { + width: 100%; + height: 8px; + background-color: #f0f0f0; + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; + } + + .download-progress-fill { + height: 100%; + background: linear-gradient(90deg, #5096ff 0%, #6b7fff 100%); + border-radius: 4px; + transition: width 0.3s ease; + } + + .download-progress-text { + display: block; + text-align: center; + font-size: 13px; + color: #5096ff; + font-weight: 500; + } + + .update-button-wrapper { + margin-top: 10px; + } + + .update-button { + width: 100%; + height: 50px; + background: linear-gradient(135deg, #5096ff 0%, #6b7fff 100%); + border-radius: 25px; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 4px 15px rgba(80, 150, 255, 0.3); + transition: all 0.3s ease; + } + + .update-button:active { + transform: scale(0.98); + box-shadow: 0 2px 8px rgba(80, 150, 255, 0.2); + } + + .update-button-disabled { + opacity: 0.7; + } + + .update-button-text { + font-size: 17px; + font-weight: 600; + color: #fff; + } + + .update-close-btn { + position: absolute; + bottom: -50px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 40px; + background-color: rgba(255, 255, 255, 0.9); + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + + .update-close-icon { + font-size: 20px; + color: #666; + font-weight: 300; + line-height: 1; + } \ No newline at end of file diff --git a/Store_vue/components/UpdateDialog.vue b/Store_vue/components/UpdateDialog.vue deleted file mode 100644 index 296b9300..00000000 --- a/Store_vue/components/UpdateDialog.vue +++ /dev/null @@ -1,556 +0,0 @@ - - - - - - diff --git a/Store_vue/components/UpdateModal.vue b/Store_vue/components/UpdateModal.vue deleted file mode 100644 index 8de78f38..00000000 --- a/Store_vue/components/UpdateModal.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - diff --git a/Store_vue/manifest.json b/Store_vue/manifest.json index 7beae997..c7fa191b 100644 --- a/Store_vue/manifest.json +++ b/Store_vue/manifest.json @@ -2,8 +2,8 @@ "name" : "AI数智员工", "appid" : "__UNI__9421F6C", "description" : "", - "versionName" : "1.1.0", - "versionCode" : 100, + "versionName" : "1.1.1", + "versionCode" : "100", "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -107,17 +107,5 @@ }, "vueVersion" : "2", "locale" : "zh-Hans", - "fallbackLocale" : "zh-Hans", - /* H5特有相关 */ - "h5" : { - "router" : { - "mode" : "hash", - "base" : "./" - }, - "title" : "AI数智员工", - "devServer" : { - "port" : 8080, - "disableHostCheck" : true - } - } + "fallbackLocale" : "zh-Hans" } diff --git a/Store_vue/package.json b/Store_vue/package.json index cf60374f..de7626df 100644 --- a/Store_vue/package.json +++ b/Store_vue/package.json @@ -1,6 +1,6 @@ { "name": "store", - "version": "1.0.0", + "version": "1.1.1", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/Store_vue/pages.json b/Store_vue/pages.json index 6a6c25ed..427192f0 100644 --- a/Store_vue/pages.json +++ b/Store_vue/pages.json @@ -4,15 +4,15 @@ }, "pages": [ { - "path": "pages/login/index", + "path": "pages/chat/index", "style": { + "navigationBarTitleText": "AI数智员工", "navigationStyle": "custom" } }, { - "path": "pages/chat/index", + "path": "pages/login/index", "style": { - "navigationBarTitleText": "AI数智员工", "navigationStyle": "custom" } } diff --git a/Store_vue/pages/login/index.vue b/Store_vue/pages/login/index.vue index 9b8a7da5..b3ce9ca2 100644 --- a/Store_vue/pages/login/index.vue +++ b/Store_vue/pages/login/index.vue @@ -1,214 +1,138 @@