Browse Source

修复副屏显示相关的bug

张洋 6 days ago
parent
commit
759a26f1e2

+ 116 - 44
zhipuzi_pos_windows/tool/CShuangpingTool.cpp

@@ -1,6 +1,8 @@
 #include "pch.h"
 #include "CShuangpingTool.h"
 
+#include <vector>
+
 CShuangpingTool::CShuangpingTool()
 {
 }
@@ -10,75 +12,103 @@ bool CShuangpingTool::IsShuangping()
 	return m_is_shuangping;
 }
 
-void CShuangpingTool::Init()
+// EnumDisplayMonitors 的回调:把所有可见显示器的屏幕矩形收进来
+static BOOL CALLBACK ShuangpingMonitorEnumProc(HMONITOR /*hMonitor*/, HDC /*hdcMonitor*/, LPRECT lprcMonitor, LPARAM dwData)
 {
-	INT iNumber = GetSystemMetrics(SM_CMONITORS);
+	std::vector<RECT>* pRects = reinterpret_cast<std::vector<RECT>*>(dwData);
+	pRects->push_back(*lprcMonitor);
+	return TRUE;
+}
 
-	if (iNumber > 1)
+void CShuangpingTool::Detect()
+{
+	m_is_shuangping = false;
+	::SetRectEmpty(&m_fuping_rect);
+
+	std::vector<RECT> rects;
+	if (!::EnumDisplayMonitors(NULL, NULL, ShuangpingMonitorEnumProc, reinterpret_cast<LPARAM>(&rects)))
 	{
-		m_is_shuangping = true;
+		LOG_INFO("副屏检测:EnumDisplayMonitors 失败,LastError=" << GetLastError());
+		return;
 	}
-	else
-	{
-		//这种情况说明没有副屏
-		m_is_shuangping = false;
 
+	if (rects.size() <= 1)
+	{
+		//只有一个显示器,不是双屏
 		return;
 	}
 
-	//确定副屏显示区域
-	RECT rect;
-	rect.left = 0;
-	rect.top = 0;
-	rect.right = 0;
-	rect.bottom = 0;
-
-	for (int i = 0; i < iNumber; i++)
+	//遍历所有显示器,找到“非主屏”的那一块作为副屏
+	for (const RECT& rc : rects)
 	{
-		//初始化
-		BOOL bFlag;
-
-		DISPLAY_DEVICE dd;
-		ZeroMemory(&dd, sizeof(dd));
-		dd.cb = sizeof(dd);
-
-		DEVMODE devMode;
-		ZeroMemory(&devMode, sizeof(devMode));
-		devMode.dmSize = sizeof(devMode);
-
-		bFlag = EnumDisplayDevices(NULL, i, &dd, 0);//如果有该显示器,再去获得它的设置
-		if (bFlag == FALSE)
+		HMONITOR hMon = ::MonitorFromRect(&rc, MONITOR_DEFAULTTONULL);
+		if (hMon == NULL)
 		{
 			continue;
 		}
 
-		bFlag = EnumDisplaySettings(dd.DeviceName, ENUM_CURRENT_SETTINGS, &devMode);
-		if (bFlag == FALSE)
+		MONITORINFO mi = { sizeof(mi) };
+		if (!::GetMonitorInfo(hMon, &mi))
 		{
 			continue;
 		}
 
-		if (devMode.dmPosition.x == 0 && devMode.dmPosition.y == 0)
+		if ((mi.dwFlags & MONITORINFOF_PRIMARY) == 0)
 		{
-			//主显示器
-			continue;
+			//非主屏:用 rcMonitor(含任务栏区域的整块屏幕)铺满副屏
+			m_fuping_rect = mi.rcMonitor;
+			m_is_shuangping = true;
+			LOG_INFO("副屏检测:找到副屏,区域=(" << m_fuping_rect.left << "," << m_fuping_rect.top
+				<< "," << m_fuping_rect.right << "," << m_fuping_rect.bottom << ")");
+			return;
 		}
-		else
-		{
-			//副显示器
-			rect.left = devMode.dmPosition.x;
-			rect.top = devMode.dmPosition.y;
+	}
 
-			rect.right = rect.left + devMode.dmPelsWidth;
-			rect.bottom = rect.top + devMode.dmPelsHeight;
+	LOG_INFO("副屏检测:检测到多块显示器但未找到有效的非主屏,不创建副屏窗口");
+}
 
-			m_fuping_rect = rect;
-		}
+void CShuangpingTool::EnsureDetected()
+{
+	std::lock_guard<std::mutex> lock(m_detectMutex);
+	if (!m_bDetected)
+	{
+		Detect();
+		m_bDetected = true;
 	}
 }
 
+void CShuangpingTool::Init()
+{
+	//程序启动时(可能在异步线程里)先做一次检测;InitWnd 还会再 EnsureDetected 兜底
+	EnsureDetected();
+}
+
 void CShuangpingTool::InitWnd(HWND hwnd)
 {
+	m_hMainWnd = hwnd;
+
+	//兜底检测:不依赖异步 Init 的完成时机,避免拿到未初始化/全零的副屏区域
+	EnsureDetected();
+
+	if (!m_is_shuangping)
+	{
+		LOG_INFO("非双屏模式,不创建副屏窗口");
+		return;
+	}
+
+	//区域有效性校验:宽高必须大于 0,否则 MoveWindow 会变成零尺寸不可见窗口
+	if (m_fuping_rect.right <= m_fuping_rect.left || m_fuping_rect.bottom <= m_fuping_rect.top)
+	{
+		LOG_INFO("副屏区域无效(零尺寸),不创建副屏窗口");
+		return;
+	}
+
+	if (m_pFuping != NULL)
+	{
+		//已经创建过,避免重复创建
+		return;
+	}
+
 	//创建并初始化副屏窗口
 	m_pFuping = new CFupingWnd();
 	if (m_pFuping == NULL)
@@ -91,10 +121,46 @@ void CShuangpingTool::InitWnd(HWND hwnd)
 	m_pFuping->Create(hwnd, _T("智铺子收银系统副屏"), UI_WNDSTYLE_DIALOG, 0, 0, 0, 0, 0, NULL);
 	m_pFuping->SetIcon(IDI_ICON_DUILIB);
 
-	::MoveWindow(m_pFuping->GetHWND(), m_fuping_rect.left, m_fuping_rect.top, m_fuping_rect.right - m_fuping_rect.left, m_fuping_rect.bottom - m_fuping_rect.top, true);
+	::MoveWindow(m_pFuping->GetHWND(), m_fuping_rect.left, m_fuping_rect.top,
+		m_fuping_rect.right - m_fuping_rect.left, m_fuping_rect.bottom - m_fuping_rect.top, TRUE);
 	m_pFuping->ShowWindow(true);
 }
 
+void CShuangpingTool::OnDisplayChange()
+{
+	bool bOldShuang = m_is_shuangping;
+	RECT oldRect = m_fuping_rect;
+
+	//强制重新检测当前显示器拓扑(加锁,与异步 Init / InitWnd 互斥)
+	{
+		std::lock_guard<std::mutex> lock(m_detectMutex);
+		m_bDetected = false;
+		Detect();
+		m_bDetected = true;
+	}
+
+	if (bOldShuang && !m_is_shuangping)
+	{
+		LOG_INFO("显示器拓扑变化:副屏丢失,销毁副屏窗口");
+		DestoryWnd();
+	}
+	else if (!bOldShuang && m_is_shuangping)
+	{
+		LOG_INFO("显示器拓扑变化:检测到副屏,创建副屏窗口");
+		InitWnd(m_hMainWnd);
+	}
+	else if (m_is_shuangping && (oldRect.left != m_fuping_rect.left || oldRect.top != m_fuping_rect.top
+		|| oldRect.right != m_fuping_rect.right || oldRect.bottom != m_fuping_rect.bottom))
+	{
+		LOG_INFO("显示器拓扑变化:副屏区域变化,移动副屏窗口");
+		if (m_pFuping != NULL)
+		{
+			::MoveWindow(m_pFuping->GetHWND(), m_fuping_rect.left, m_fuping_rect.top,
+				m_fuping_rect.right - m_fuping_rect.left, m_fuping_rect.bottom - m_fuping_rect.top, TRUE);
+		}
+	}
+}
+
 void CShuangpingTool::DestoryWnd()
 {
 	if (m_pFuping == NULL)
@@ -102,8 +168,14 @@ void CShuangpingTool::DestoryWnd()
 		return;
 	}
 
+	//先停止副屏内部的工作线程并杀掉定时器
 	m_pFuping->Stop();
 
+	//销毁窗口(会触发 CFupingWnd::OnFinalMessage,但 OnFinalMessage 里不再 delete this)
+	::DestroyWindow(m_pFuping->GetHWND());
+
+	//统一由 CShuangpingTool 持有并释放副屏对象,避免二次释放/悬空指针
+	delete m_pFuping;
 	m_pFuping = NULL;
 }
 

+ 21 - 1
zhipuzi_pos_windows/tool/CShuangpingTool.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#include <mutex>
+
 #include "../wnd/CFupingWnd.h"
 #include "../zhipuzi/CDiandanOrder.h"
 #include "../zhipuzi/CZhengcanOrder.h"
@@ -17,9 +19,18 @@ public:
 	//初始化副屏,对副屏情况进行判断(程序刚启动马上就执行),但是不创建副屏窗口
 	void Init();
 
-	//登录成功后,初始化副屏窗口
+	//真正去枚举显示器、判断是否为双屏并计算副屏区域
+	void Detect();
+
+	//确保已经做过一次检测(幂等),InitWnd 在创建窗口前调用,消除与异步 Init 的竞态
+	void EnsureDetected();
+
+	//登录成功后,初始化副屏窗口(先 EnsureDetected,再判定是否真的需要创建)
 	void InitWnd(HWND hwnd);
 
+	//显示器拓扑发生变化(热插拔 / 分辨率变化)时调用,按需创建/销毁/移动副屏窗口
+	void OnDisplayChange();
+
 	//退出登录,或者程序退出后,销毁副屏
 	void DestoryWnd();
 
@@ -42,5 +53,14 @@ private:
 	RECT m_fuping_rect;
 
 	CFupingWnd* m_pFuping = NULL;
+
+	//是否已经做过显示器检测(Init/Detect 置 true),EnsureDetected 据此去重
+	bool m_bDetected = false;
+
+	//主窗口句柄,用于创建/重建副屏窗口时作为父窗口
+	HWND m_hMainWnd = NULL;
+
+	//保护显示器检测的互斥量(Init 在异步线程、InitWnd/OnDisplayChange 在 UI 线程都会触发检测)
+	std::mutex m_detectMutex;
 };
 

+ 92 - 43
zhipuzi_pos_windows/wnd/CFupingWnd.cpp

@@ -70,6 +70,14 @@ LRESULT CFupingWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
 	case WM_SIZE:
 		lRes = OnSize(uMsg, wParam, lParam, bHandled);
 		break;
+	case WM_TIMER:
+		lRes = OnTimer(uMsg, wParam, lParam, bHandled);
+		break;
+	case WM_LUNBO_IMAGE_READY:
+		//后台线程首次拿到轮播图后主动通知,UI 线程立即出图(自愈路径加速)
+		AdvanceCarousel();
+		bHandled = TRUE;
+		break;
 	default:
 		bHandled = FALSE;
 	}
@@ -134,76 +142,108 @@ void CFupingWnd::Notify(TNotifyUI& msg)
 	
 }
 
-//刚启动程序初始化副屏窗口的时候执行,app生命周期只执行一次
+//刚启动程序初始化副屏窗口的时候执行,app生命周期只执行一次(在 UI 线程 OnCreate 里调用)
 void CFupingWnd::Init()
 {
 	m_is_work = true;
 
-	//启动轮播图展示任务
-	std::thread(&CFupingWnd::HandleFupingImage, this).detach();
+	//轮播由 UI 线程的定时器驱动(5 秒切换一次),避免在后台线程里直接操作 DuiLib 引发跨线程问题
+	::SetTimer(GetHWND(), TIMER_LUNBO_CAROUSEL, 5000, NULL);
+
+	//后台数据线程:只做网络/磁盘(请求 + 下载轮播图),绝不碰 DuiLib,
+	//从而消除跨线程访问 DuiLib 与 m_lunbo_img_paths 的数据竞争
+	m_workerThread = std::thread(&CFupingWnd::LunboWorker, this);
+
+	//★立即渲染首图:副屏窗口是在登录成功后才创建的(ShowLoginSuccess -> new CMainWnd -> InitWnd),
+	//此时轮播图已由 UpdateOneShopinfo 同步下载好、m_lunbo_img_paths 已就绪。
+	//若只依赖 5 秒定时器,首图要等定时器第一次触发(约 5 秒)才显示,就会出现"登录后绿底空白约 5 秒"。
+	AdvanceCarousel();
 }
 
-void CFupingWnd::HandleFupingImage()
+void CFupingWnd::LunboWorker()
 {
-	while (m_is_work == true)
+	while (m_is_work.load())
 	{
-		if (CShopinfo::GetInstance()->m_shop_name.size() == 0)
+		if (!CShopinfo::GetInstance()->IsLoggedIn())
 		{
-			//还没登录成功,没拿到店铺名字,等待
+			//还没登录成功,没拿到店铺信息,等待
 			CSystem::my_sleep(1);
-
 			continue;
 		}
 
-		//更新店铺名字
-		CLabelUI* nameControl = static_cast<CLabelUI*>(m_pm.FindControl(L"shopname"));
-		std::wstring shopname = CLewaimaiString::UTF8ToUnicode(CShopinfo::GetInstance()->m_shop_name);
-		nameControl->SetText(shopname.c_str());
+		//自愈:空列表 / 缺图时(内部带 30s 节流)重新请求后台并补下缺失图片
+		CShopinfo::GetInstance()->RefreshLunboImages(false);
 
-		std::vector<std::wstring> lunbo_img_paths = CShopinfo::GetInstance()->m_lunbo_img_paths;
-		if (lunbo_img_paths.size() == 0)
+		//若此时还没成功显示过任何轮播图(例如登录瞬间拉图失败、或首图下载失败),
+		//一旦拿到可用图片就立即通知 UI 线程出图,无需再等 5 秒定时器
+		if (!m_bHasImage.load() && !CShopinfo::GetInstance()->GetLunboImgPaths().empty())
 		{
-			//有可能后台没设置轮播图,有可能还没请求到后台最新数据,等待
-			CSystem::my_sleep(2);
-
-			continue;
+			::PostMessage(m_hWnd, WM_LUNBO_IMAGE_READY, 0, 0);
 		}
 
-		//然后开始处理轮播图
-		if (m_curImageNum > lunbo_img_paths.size() - 1)
-		{
-			m_curImageNum = 0;
-		}
+		//5 秒后再检查一次
+		CSystem::my_sleep(5);
+	}
 
-		std::wstring image_path = lunbo_img_paths[m_curImageNum];
+	//线程结束,不要在这里 delete this(生命周期由 CShuangpingTool 统一管理)
+}
 
-		//判断图片是否存在
-		if (!CSystem::IsFileExist(image_path))
-		{
-			//轮播图的图片还未下载好
-			CSystem::my_sleep(3);
+//UI 线程定时器回调:每 5 秒切换一次轮播图(只在 UI 线程操作 DuiLib,线程安全)
+void CFupingWnd::AdvanceCarousel()
+{
+	//更新店铺名字(判空,避免控件缺失时崩)
+	CLabelUI* nameControl = static_cast<CLabelUI*>(m_pm.FindControl(L"shopname"));
+	if (nameControl != NULL)
+	{
+		std::wstring shopname = CLewaimaiString::UTF8ToUnicode(CShopinfo::GetInstance()->GetShopName());
+		nameControl->SetText(shopname.c_str());
+	}
 
-			//3秒后还没下载好,处理下一张
-			m_curImageNum++;
+	//读取轮播图路径(线程安全拷贝)
+	std::vector<std::wstring> lunbo_img_paths = CShopinfo::GetInstance()->GetLunboImgPaths();
+	if (lunbo_img_paths.empty())
+	{
+		//后台还没拿到轮播图(自愈线程会去拉取),保持当前绿底即可
+		return;
+	}
 
-			continue;
-		}
+	if (m_curImageNum >= lunbo_img_paths.size())
+	{
+		m_curImageNum = 0;
+	}
 
-		//轮播图的图片已经下载好了,那么就直接渲染
-		CControlUI* imageControl = static_cast<CControlUI*>(m_pm.FindControl(L"lunbo_image"));
-		imageControl->SetBkImage(image_path.c_str());
+	std::wstring image_path = lunbo_img_paths[m_curImageNum];
 
-		//等待5秒钟,换下一张
-		CSystem::my_sleep(5);
+	//图片还没下好:跳过这一张,下个周期再试(自愈线程会补下)
+	if (!CSystem::IsFileExist(image_path))
+	{
+		m_curImageNum = (m_curImageNum + 1) % lunbo_img_paths.size();
+		return;
+	}
+
+	//轮播图的图片已经下载好了,渲染到控件(判空)
+	CControlUI* imageControl = static_cast<CControlUI*>(m_pm.FindControl(L"lunbo_image"));
+	if (imageControl != NULL)
+	{
+		imageControl->SetBkImage(image_path.c_str());
+		m_bHasImage = true;
+	}
 
-		//5秒轮播一次
-		m_curImageNum++;
+	//5 秒轮播一次
+	m_curImageNum = (m_curImageNum + 1) % lunbo_img_paths.size();
+}
 
-		continue;
+LRESULT CFupingWnd::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+{
+	if (wParam == TIMER_LUNBO_CAROUSEL)
+	{
+		AdvanceCarousel();
+		bHandled = TRUE;
+		return 0;
 	}
 
-	//这个线程结束后,释放自己
-	delete this;
+	bHandled = FALSE;
+	return 0;
 }
 
 //刷新副屏显示
@@ -491,4 +531,13 @@ void CFupingWnd::UpdateShow(CZhengcanOrder& order, double total_youhui, double t
 void CFupingWnd::Stop()
 {
 	m_is_work = false;
+
+	//停掉轮播定时器
+	::KillTimer(GetHWND(), TIMER_LUNBO_CAROUSEL);
+
+	//等待后台数据线程结束(线程里只做网络/磁盘,不会死等 UI)
+	if (m_workerThread.joinable())
+	{
+		m_workerThread.join();
+	}
 }

+ 27 - 4
zhipuzi_pos_windows/wnd/CFupingWnd.h

@@ -2,6 +2,9 @@
 
 #include "pch.h"
 
+#include <atomic>
+#include <thread>
+
 #include "../zhipuzi/CDiandanOrder.h"
 #include "../zhipuzi/CZhengcanOrder.h"
 
@@ -59,12 +62,18 @@ public:
 
 	LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, bool& bHandled);
 
-	//窗口初始化的时候调用一次,用来处理副屏轮播图展示
-	void HandleFupingImage();
+	//窗口初始化的时候调用一次,用来启动轮播定时器与后台数据线程(Init 见上方声明)
+	//UI 线程定时器驱动的轮播切换(只碰 DuiLib,线程安全)
+	void AdvanceCarousel();
+
+	//后台工作线程:只做网络/磁盘(请求 + 下载轮播图),绝不碰 DuiLib
+	void LunboWorker();
 
-	//停止刷新副屏的线程,程序退出
+	//停止刷新副屏,程序退出
 	void Stop();
 
+	LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
+
 	//刷新副屏显示
 	void UpdateShow(CDiandanOrder& order, double total_youhui, double total_price, double dabao_money);
 
@@ -76,6 +85,20 @@ public:
 private:
 	size_t m_curImageNum = 0;
 
-	bool m_is_work;
+	//是否已成功渲染过至少一张轮播图(UI 线程写、后台线程读,用原子避免数据竞争)
+	std::atomic<bool> m_bHasImage{false};
+
+	//工作线程退出标志(原子,避免与后台线程的数据竞争)
+	std::atomic<bool> m_is_work{false};
+
+	//后台数据线程(可 join,不在内部 delete this)
+	std::thread m_workerThread;
+
+	//轮播定时器 ID(用较大的值,避免与 DuiLib 内部控件定时器 ID 冲突)
+	static const UINT TIMER_LUNBO_CAROUSEL = 1001;
+
+	//自定义消息:后台线程拿到轮播图后,通知 UI 线程立即渲染
+	//(避免仍要等下一个 5 秒定时器才出图)
+	static const UINT WM_LUNBO_IMAGE_READY = WM_APP + 101;
 };
 

+ 5 - 0
zhipuzi_pos_windows/wnd/CMainWnd.cpp

@@ -481,6 +481,11 @@ LRESULT CMainWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
 	case WM_KILLFOCUS:
 		break;
 
+	case WM_DISPLAYCHANGE:
+		//显示器拓扑变化(热插拔 / 分辨率变化):按需创建/销毁/移动副屏窗口
+		CShuangpingTool::GetInstance()->OnDisplayChange();
+		break;
+
     default:
         bHandled = FALSE;
     }

+ 140 - 29
zhipuzi_pos_windows/zhipuzi/CShopinfo.cpp

@@ -77,6 +77,9 @@ void CShopinfo::InitShopinfoByLogin(rapidjson::Value& v_data)
 	this->m_is_gouwuka = CLewaimaiJson::ToString(v_authority["is_gouwuka"]);
 	this->m_is_gouwuka_jihuo = CLewaimaiJson::ToString(v_authority["is_gouwuka_jihuo"]);
 	this->m_is_meishicheng = CLewaimaiJson::ToString(v_authority["is_meishicheng"]);
+
+	//登录信息已就绪,通知副屏后台线程可以开始拉取轮播图(先于 m_shop_name 写后可见)
+	m_bLoggedIn = true;
 }
 
 std::wstring CShopinfo::GetImageTmpPathFromUrl(std::string image_url)
@@ -297,39 +300,20 @@ bool CShopinfo::UpdateOneShopinfo(std::wstring& errmsg)
 		}
 	}
 
-	//开始下载副屏轮播图
+	//开始下载副屏轮播图(走统一的、线程安全的取图 + 落盘逻辑)
 	if (CShuangpingTool::GetInstance()->IsShuangping())
 	{
-		std::vector<std::string> lunbo_imgs;
-		lunbo_imgs.clear();
-
-		m_lunbo_img_paths.clear();
+		std::wstring errmsg;
+		std::vector<std::wstring> newPaths = FetchLunboImagePaths(errmsg);
+		if (newPaths.empty() && !errmsg.empty())
+		{
+			LOG_INFO(L"副屏轮播图初始化失败: " << errmsg.c_str());
+		}
 
-		std::wstring lunbo_errmsg;
-		bool ret = GetLunboImages(lunbo_imgs, lunbo_errmsg);
-		if (ret)
 		{
-			//开始下载轮播图图片到本地
-			for (std::vector<std::string>::iterator it = lunbo_imgs.begin(); it != lunbo_imgs.end(); it++)
-			{
-				std::string image_url = *it;
-
-				std::wstring imagePath = GetImageTmpPathFromUrl(image_url);
-				m_lunbo_img_paths.push_back(imagePath);
-
-				if (!CSystem::IsFileExist(imagePath))
-				{
-					//图片还不存在,开始下载
-					if (URLDownloadToFile(NULL, CLewaimaiString::UTF8ToUnicode(image_url).c_str(), imagePath.c_str(), 0, NULL) == S_OK)
-					{
-						//图片下载成功了,发个消息,更新图片
-					}
-					else
-					{
-						LOG_INFO("URLDownloadToFile Fail,Error" << GetLastError());
-					}
-				}
-			}
+			std::lock_guard<std::mutex> lock(m_lunboMutex);
+			m_lunbo_img_paths = std::move(newPaths);
+			m_lastLunboFetchTime = std::chrono::steady_clock::now();
 		}
 	}
 
@@ -410,6 +394,133 @@ bool CShopinfo::GetLunboImages(std::vector<std::string>& images, std::wstring& e
 	return true;
 }
 
+std::vector<std::wstring> CShopinfo::FetchLunboImagePaths(std::wstring& errmsg)
+{
+	std::vector<std::wstring> result;
+	errmsg.clear();
+
+	if (!CShuangpingTool::GetInstance()->IsShuangping())
+	{
+		return result;
+	}
+
+	std::vector<std::string> lunbo_imgs;
+	bool ret = GetLunboImages(lunbo_imgs, errmsg);
+	if (!ret)
+	{
+		return result;  // errmsg 已由 GetLunboImages 填写
+	}
+
+	for (std::vector<std::string>::iterator it = lunbo_imgs.begin(); it != lunbo_imgs.end(); it++)
+	{
+		std::string image_url = *it;
+
+		std::wstring imagePath = GetImageTmpPathFromUrl(image_url);
+		result.push_back(imagePath);
+
+		if (!CSystem::IsFileExist(imagePath))
+		{
+			//图片还不存在,开始下载
+			if (URLDownloadToFile(NULL, CLewaimaiString::UTF8ToUnicode(image_url).c_str(), imagePath.c_str(), 0, NULL) == S_OK)
+			{
+				//图片下载成功了
+			}
+			else
+			{
+				LOG_INFO("URLDownloadToFile Fail,Error" << GetLastError());
+			}
+		}
+	}
+
+	return result;
+}
+
+std::vector<std::wstring> CShopinfo::GetLunboImgPaths()
+{
+	std::lock_guard<std::mutex> lock(m_lunboMutex);
+	return m_lunbo_img_paths;
+}
+
+bool CShopinfo::IsLoggedIn()
+{
+	return m_bLoggedIn.load(std::memory_order_acquire);
+}
+
+std::string CShopinfo::GetShopName()
+{
+	// m_shop_name 只在登录时写入一次,读取发生在登录之后;由 m_bLoggedIn 的 memory order 保证可见性
+	return m_shop_name;
+}
+
+void CShopinfo::RefreshLunboImages(bool bForce)
+{
+	if (!CShuangpingTool::GetInstance()->IsShuangping())
+	{
+		return;
+	}
+
+	auto now = std::chrono::steady_clock::now();
+	bool needFetch = false;
+
+	//先拷贝当前路径,避免持锁做磁盘 IO
+	std::vector<std::wstring> paths;
+	{
+		std::lock_guard<std::mutex> lock(m_lunboMutex);
+		paths = m_lunbo_img_paths;
+	}
+
+	if (bForce || paths.empty())
+	{
+		//空列表时必须重取;强制时也重取。用 30s 节流避免频繁打后台
+		std::lock_guard<std::mutex> lock(m_lunboMutex);
+		auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_lastLunboFetchTime).count();
+		if (bForce || elapsed >= 30)
+		{
+			m_lastLunboFetchTime = now;
+			needFetch = true;
+		}
+	}
+	else
+	{
+		//已有数据:检查是否有缺失,有缺失才补下(同样 30s 节流)
+		bool allExist = true;
+		for (auto& p : paths)
+		{
+			if (!CSystem::IsFileExist(p))
+			{
+				allExist = false;
+				break;
+			}
+		}
+
+		if (!allExist)
+		{
+			std::lock_guard<std::mutex> lock(m_lunboMutex);
+			auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_lastLunboFetchTime).count();
+			if (elapsed >= 30)
+			{
+				m_lastLunboFetchTime = now;
+				needFetch = true;
+			}
+		}
+	}
+
+	if (needFetch)
+	{
+		std::wstring errmsg;
+		std::vector<std::wstring> newPaths = FetchLunboImagePaths(errmsg);
+		if (newPaths.empty() && !errmsg.empty())
+		{
+			LOG_INFO(L"副屏轮播图刷新失败: " << errmsg.c_str());
+		}
+
+		{
+			std::lock_guard<std::mutex> lock(m_lunboMutex);
+			m_lunbo_img_paths = std::move(newPaths);
+		}
+	}
+}
+
 bool CShopinfo::GetTabletypeInfo()
 {
 	std::map<std::string, std::string> params;

+ 30 - 0
zhipuzi_pos_windows/zhipuzi/CShopinfo.h

@@ -1,5 +1,10 @@
 #pragma once
 
+#include <atomic>
+#include <chrono>
+#include <mutex>
+#include <vector>
+
 class AddService
 {
 public:
@@ -68,9 +73,25 @@ public:
 
 	bool GetLunboImages(std::vector<std::string>& images, std::wstring& errmsg);
 
+	//线程安全:拷贝出当前轮播图本地路径列表
+	std::vector<std::wstring> GetLunboImgPaths();
+
+	//线程安全:刷新(重新请求 + 下载)轮播图;bForce 强制重取;空列表 / 缺图时自愈(内部 30s 节流);失败打印日志
+	void RefreshLunboImages(bool bForce);
+
+	//是否已登录(店铺名已就绪),供副屏后台线程安全判断
+	bool IsLoggedIn();
+
+	//线程安全拷贝店铺名(UTF-8),供副屏线程使用
+	std::string GetShopName();
+
 	//获取店铺的桌位分类信息,保存起来,后续不用再重复获取了
 	bool GetTabletypeInfo();
 
+private:
+	//真正去后台请求 + 下载轮播图,返回本地路径列表(不加锁,由调用方加锁/记日志)
+	std::vector<std::wstring> FetchLunboImagePaths(std::wstring& errmsg);
+
 public:
 	//当前系统版本 1餐饮版 2零售版
 	std::string m_version_type;
@@ -195,6 +216,15 @@ public:
 	//这个存放的是轮播图副屏在本地对应的文件路径
 	std::vector<std::wstring> m_lunbo_img_paths;
 
+	//保护 m_lunbo_img_paths 读写的互斥量(副屏 UI 线程读、后台线程 / 登录线程写)
+	std::mutex m_lunboMutex;
+
+	//上次成功 / 尝试请求轮播图的时间戳(用于 30s 节流,避免频繁打后台)
+	std::chrono::steady_clock::time_point m_lastLunboFetchTime{};
+
+	//是否已登录(店铺名已就绪),避免副屏线程与登录线程竞争读写 m_shop_name
+	std::atomic<bool> m_bLoggedIn{false};
+
 	//把桌位分类信息一起保存起来
 	std::vector<TableType> m_tabletypes;
 };