修正倒计时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