修正倒计时bug 更新导税员窗口
parent
0eb502ae43
commit
0533c2ccb7
@ -0,0 +1,180 @@
|
|||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StartScreenSyncPayload {
|
||||||
|
pub notify_base_url: String,
|
||||||
|
pub stream_url: String,
|
||||||
|
pub display: Option<String>,
|
||||||
|
pub frame_rate: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StopScreenSyncPayload {
|
||||||
|
pub notify_base_url: Option<String>,
|
||||||
|
pub stream_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_base_url(raw: &str) -> String {
|
||||||
|
raw.trim().trim_end_matches('/').to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_sync_event(
|
||||||
|
notify_base_url: &str,
|
||||||
|
path: &str,
|
||||||
|
stream_url: Option<&str>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let endpoint = format!("{}/{}", normalize_base_url(notify_base_url), path);
|
||||||
|
let mut body = serde_json::Map::new();
|
||||||
|
if let Some(url) = stream_url {
|
||||||
|
body.insert("url".to_string(), json!(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::builder()
|
||||||
|
.timeout(Duration::from_secs(5))
|
||||||
|
.build()
|
||||||
|
.map_err(|error| format!("创建 HTTP 客户端失败: {error}"))?
|
||||||
|
.post(endpoint)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.and_then(|response| response.error_for_status())
|
||||||
|
.map_err(|error| format!("通知同步服务失败: {error}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill_child_process(child: &mut std::process::Child) {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_screen_sync(state: &AppState) {
|
||||||
|
let (notify_base_url, stream_url) = {
|
||||||
|
let mut guard = match state.screen_sync.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(child) = guard.ffmpeg.as_mut() {
|
||||||
|
kill_child_process(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
let notify = guard.notify_base_url.clone();
|
||||||
|
let stream = guard.stream_url.clone();
|
||||||
|
guard.ffmpeg = None;
|
||||||
|
guard.notify_base_url = None;
|
||||||
|
guard.stream_url = None;
|
||||||
|
(notify, stream)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(base_url) = notify_base_url {
|
||||||
|
let _ = post_sync_event(&base_url, "sync/stop", stream_url.as_deref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn start_screen_sync(
|
||||||
|
state: State<AppState>,
|
||||||
|
payload: StartScreenSyncPayload,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let notify_base_url = normalize_base_url(payload.notify_base_url.as_str());
|
||||||
|
let stream_url = payload.stream_url.trim().to_string();
|
||||||
|
if notify_base_url.is_empty() || stream_url.is_empty() {
|
||||||
|
return Err("同步参数不完整".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let display = payload.display.unwrap_or_else(|| ":0.0".to_string());
|
||||||
|
let frame_rate = payload.frame_rate.unwrap_or(15).to_string();
|
||||||
|
|
||||||
|
let mut command = Command::new("ffmpeg");
|
||||||
|
command
|
||||||
|
.arg("-f")
|
||||||
|
.arg("x11grab")
|
||||||
|
.arg("-framerate")
|
||||||
|
.arg(frame_rate)
|
||||||
|
.arg("-i")
|
||||||
|
.arg(display)
|
||||||
|
.arg("-vcodec")
|
||||||
|
.arg("libx264")
|
||||||
|
.arg("-preset")
|
||||||
|
.arg("veryfast")
|
||||||
|
.arg("-tune")
|
||||||
|
.arg("zerolatency")
|
||||||
|
.arg("-f")
|
||||||
|
.arg("flv")
|
||||||
|
.arg(stream_url.as_str())
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
|
let child = command
|
||||||
|
.spawn()
|
||||||
|
.map_err(|error| format!("启动 FFmpeg 失败: {error}"))?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut guard = state
|
||||||
|
.screen_sync
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| "同步状态锁异常".to_string())?;
|
||||||
|
if guard.ffmpeg.is_some() {
|
||||||
|
let mut spawned = child;
|
||||||
|
kill_child_process(&mut spawned);
|
||||||
|
return Err("同步主屏已在进行中".to_string());
|
||||||
|
}
|
||||||
|
guard.ffmpeg = Some(child);
|
||||||
|
guard.notify_base_url = Some(notify_base_url.clone());
|
||||||
|
guard.stream_url = Some(stream_url.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = post_sync_event(¬ify_base_url, "sync/start", Some(stream_url.as_str())) {
|
||||||
|
cleanup_screen_sync(&state);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn stop_screen_sync(
|
||||||
|
state: State<AppState>,
|
||||||
|
payload: Option<StopScreenSyncPayload>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (mut child, notify_base_url, stream_url) = {
|
||||||
|
let mut guard = state
|
||||||
|
.screen_sync
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| "同步状态锁异常".to_string())?;
|
||||||
|
let notify = payload
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|item| item.notify_base_url.clone())
|
||||||
|
.map(|value| normalize_base_url(value.as_str()))
|
||||||
|
.or_else(|| guard.notify_base_url.clone());
|
||||||
|
let stream = payload
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|item| item.stream_url.clone())
|
||||||
|
.or_else(|| guard.stream_url.clone());
|
||||||
|
let child = guard.ffmpeg.take();
|
||||||
|
guard.notify_base_url = None;
|
||||||
|
guard.stream_url = None;
|
||||||
|
(child, notify, stream)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(process) = child.as_mut() {
|
||||||
|
kill_child_process(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(base_url) = notify_base_url {
|
||||||
|
post_sync_event(&base_url, "sync/stop", stream_url.as_deref())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { getSession } from "../host/session";
|
||||||
|
|
||||||
|
type JsonRecord = Record<string, unknown>;
|
||||||
|
const MOCK_BASE_URL = "http://127.0.0.1:4523/m1/8201806-7961256-default";
|
||||||
|
|
||||||
|
type BaiShuiEnvelope<T> = {
|
||||||
|
code?: string;
|
||||||
|
msg?: string;
|
||||||
|
message?: string;
|
||||||
|
mess?: string;
|
||||||
|
result?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BsyxxResult = {
|
||||||
|
xm?: string;
|
||||||
|
xb?: string;
|
||||||
|
mz?: string;
|
||||||
|
sfzhm?: string;
|
||||||
|
sjhm?: string;
|
||||||
|
bsyly?: string;
|
||||||
|
zz?: string;
|
||||||
|
csrq?: string;
|
||||||
|
sfzzmPic?: string;
|
||||||
|
headPic?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SmzcjResult = {
|
||||||
|
code?: string;
|
||||||
|
sjhm?: string;
|
||||||
|
bz?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BdNsrxxItem = {
|
||||||
|
djxh?: string;
|
||||||
|
nsrsbh?: string;
|
||||||
|
nsrmc?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BdNsrxxResult = {
|
||||||
|
dataList?: BdNsrxxItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaxerEnterprise = {
|
||||||
|
serialNumber: string;
|
||||||
|
enterpriseName: string;
|
||||||
|
taxpayerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function upperCaseUrlEncode(content: string): string {
|
||||||
|
return encodeURIComponent(content).replace(/%[0-9a-f]{2}/gi, (value) => value.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRealnameApiUrl(baseOrFull: string): string {
|
||||||
|
const normalized = baseOrFull.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return `${MOCK_BASE_URL}/taxCommon/doService`;
|
||||||
|
}
|
||||||
|
if (normalized.includes("/taxCommon/doService")) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return `${normalized.replace(/\/$/, "")}/taxCommon/doService`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildBaiShuiBaseFields(): Promise<Record<string, string>> {
|
||||||
|
const session = await getSession();
|
||||||
|
const useDocFields = session.taxer_use_doc_fields === true;
|
||||||
|
if (!useDocFields) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pick = (key: string, fallback = ""): string => {
|
||||||
|
const raw = session[key as keyof typeof session];
|
||||||
|
return typeof raw === "string" ? raw.trim() : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
uid: pick("taxer_uid"),
|
||||||
|
sid: pick("taxer_sid"),
|
||||||
|
ver: pick("taxer_ver", "1.0"),
|
||||||
|
kz: pick("taxer_kz"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractResult<T>(payload: unknown, ywId: string): T {
|
||||||
|
const body = (payload ?? {}) as BaiShuiEnvelope<T>;
|
||||||
|
const code = String(body.code ?? "").trim();
|
||||||
|
if (code !== "00") {
|
||||||
|
const message = String(body.mess ?? body.message ?? body.msg ?? "接口返回失败");
|
||||||
|
throw new Error(`[${ywId}] ${message || "code != 00"}`);
|
||||||
|
}
|
||||||
|
if (body.result === undefined || body.result === null) {
|
||||||
|
throw new Error(`[${ywId}] 返回 result 为空`);
|
||||||
|
}
|
||||||
|
return body.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postBaiShui<T>(ywId: string, params: JsonRecord): Promise<T> {
|
||||||
|
const session = await getSession();
|
||||||
|
const apiUrl = resolveRealnameApiUrl(
|
||||||
|
typeof session.realnameCheckApi === "string" ? session.realnameCheckApi : "",
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
...(await buildBaiShuiBaseFields()),
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
const body = `domain.ywId=${upperCaseUrlEncode(ywId)}&domain.parmJson=${upperCaseUrlEncode(payload)}`;
|
||||||
|
|
||||||
|
const response = await axios.post(apiUrl, body, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return extractResult<T>(response.data, ywId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTaxerProfile(compareCode: string): Promise<{
|
||||||
|
bsyxx: BsyxxResult;
|
||||||
|
smzcj: SmzcjResult | null;
|
||||||
|
bdNsrxx: BdNsrxxResult | null;
|
||||||
|
}> {
|
||||||
|
const qhhm = compareCode.trim();
|
||||||
|
if (!qhhm) {
|
||||||
|
throw new Error("compareCode 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bsyxx = await postBaiShui<BsyxxResult>("smz.jhxtGetBsyxxByqhhm", { qhhm });
|
||||||
|
const sfzhm = String(bsyxx.sfzhm ?? "").trim();
|
||||||
|
if (!sfzhm) {
|
||||||
|
return { bsyxx, smzcj: null, bdNsrxx: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let smzcj: SmzcjResult | null = null;
|
||||||
|
let bdNsrxx: BdNsrxxResult | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
smzcj = await postBaiShui<SmzcjResult>("smz.getSmzcjxx", { sfzhm });
|
||||||
|
} catch {
|
||||||
|
smzcj = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
bdNsrxx = await postBaiShui<BdNsrxxResult>("smz.jhxtGetBdNsrxx", { sfzhm });
|
||||||
|
} catch {
|
||||||
|
bdNsrxx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bsyxx, smzcj, bdNsrxx };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTodayEnterprises(ticketId: string): Promise<TaxerEnterprise[]> {
|
||||||
|
const tktId = ticketId.trim();
|
||||||
|
if (!tktId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getSession();
|
||||||
|
const baseUrl =
|
||||||
|
typeof session.ziyunServiceUrl === "string" && session.ziyunServiceUrl.trim() !== ""
|
||||||
|
? session.ziyunServiceUrl.trim()
|
||||||
|
: MOCK_BASE_URL;
|
||||||
|
|
||||||
|
const url = `${baseUrl.replace(/\/$/, "")}/agentInfo/today-enterprises/${encodeURIComponent(tktId)}`;
|
||||||
|
const response = await axios.get(url, { timeout: 10000 });
|
||||||
|
const payload = response.data as { code?: number; data?: Array<Record<string, unknown>> };
|
||||||
|
if (payload.code !== 200 || !Array.isArray(payload.data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.data.map((item) => ({
|
||||||
|
serialNumber: String(item.serialNumber ?? ""),
|
||||||
|
enterpriseName: String(item.serviceTarget ?? ""),
|
||||||
|
taxpayerId: String(item.serviceTargetCode ?? ""),
|
||||||
|
}));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue