欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

在System身份運行的.NET程序中以指定的用戶身份啟動可交互式進程

freeflydom
2025年5月17日 10:2 本文熱度 72

今天在技術群里,石頭哥向大家提了個問題:"如何在一個以System身份運行的.NET程序(Windows Services)中,以其它活動的用戶身份啟動可交互式進程(桌面應用程序、控制臺程序、等帶有UI和交互式體驗的程序)"?

我以前有過類似的需求,是在GitLab流水線中運行帶有UI的自動化測試程序

其中流水線是GitLab Runner執行的,而GitLab Runner則被注冊為Windows服務,以System身份啟動的。

然后我在流水線里,巴拉巴拉寫了一大串PowerShell腳本代碼,通過調用任務計劃程序實現了這個需求

但我沒試過在C#里實現這個功能。

對此,我很感興趣,于是著手研究,最終搗鼓出來了。

二話不多說,上代碼:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace AllenCai.Windows
{
    /// <summary>
    /// 進程工具類
    /// </summary>
#if NET5_0_OR_GREATER
    [SupportedOSPlatform("windows")]
#endif
    public static class ProcessUtils
    {
        /// <summary>
        /// 在當前活動的用戶會話中啟動進程
        /// </summary>
        /// <param name="fileName">程序名稱或程序路徑</param>
        /// <param name="commandLine">命令行參數</param>
        /// <param name="workDir">工作目錄</param>
        /// <param name="noWindow">是否無窗口</param>
        /// <param name="minimize">是否最小化</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ApplicationException"></exception>
        /// <exception cref="Win32Exception"></exception>
        public static int StartProcessAsActiveUser(string fileName, string commandLine = null, string workDir = null, bool noWindow = false, bool minimize = false)
        {
            if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException(nameof(fileName));
            // 獲取當前活動的控制臺會話ID和安全的用戶訪問令牌
            IntPtr userToken = GetSessionUserToken();
            if (userToken == IntPtr.Zero)
                throw new ApplicationException("Failed to get user token for the active session.");
            IntPtr duplicateToken = IntPtr.Zero;
            IntPtr environmentBlock = IntPtr.Zero;
            try
            {
                // 復制令牌
                SecurityAttributes sa = new SecurityAttributes();
                sa.Length = Marshal.SizeOf(sa);
                if (!DuplicateTokenEx(userToken, MAXIMUM_ALLOWED, ref sa, SecurityImpersonationLevel.SecurityIdentification, TokenType.TokenPrimary, out duplicateToken))
                    throw new ApplicationException("Could not duplicate token.");
                // 創建環境塊(檢索該用戶的環境變量)
                if (!CreateEnvironmentBlock(out environmentBlock, duplicateToken, false))
                    throw new ApplicationException("Could not create environment block.");
                bool theCommandIsInPath;
                // 如果文件名不包含路徑分隔符,則嘗試先在workDir參數中查找。如果找不到,再在指定用戶會話的PATH環境變量中查找。如果還是找不到,則拋出異常
                if ((!fileName.Contains('/') && !fileName.Contains('\\')))
                {
                    if (!string.IsNullOrEmpty(workDir))
                    {
                        if (File.Exists(Path.Combine(workDir, fileName)))
                        {
                            // 在指定的工作目錄中找到可執行命令文件
                            theCommandIsInPath = false;
                        }
                        else
                        {
                            // 在指定的工作目錄(workDir)中找不到可執行命令文件,再在指定用戶會話的PATH環境變量中查找。如果還是找不到,則拋出異常
                            if (!InPathOfSpecificUserEnvironment(in duplicateToken, in environmentBlock, fileName))
                            {
                                throw new ApplicationException($"The file '{fileName}' was not found in the specified directory '{workDir}' or in the PATH environment variable.");
                            }
                            else
                            {
                                // 在指定用戶會話的PATH環境變量中找到可執行命令文件
                                theCommandIsInPath = true;
                            }
                        }
                    }
                    else
                    {
                        // 在指定用戶會話的PATH環境變量中查找,如果找不到,則拋出異常
                        if (!InPathOfSpecificUserEnvironment(in duplicateToken, in environmentBlock, fileName))
                        {
                            throw new ApplicationException($"The file '{fileName}' was not found in the PATH environment variable.");
                        }
                        // 在指定用戶會話的PATH環境變量中找到可執行命令文件
                        theCommandIsInPath = true;
                    }
                }
                else
                {
                    theCommandIsInPath = false;
                }
                string file;
                if (!theCommandIsInPath && !Path.IsPathRooted(fileName))
                {
                    file = !string.IsNullOrEmpty(workDir) ? Path.GetFullPath(Path.Combine(workDir, fileName)) : Path.GetFullPath(fileName);
                }
                else
                {
                    file = fileName;
                }
                if (string.IsNullOrWhiteSpace(workDir)) workDir = theCommandIsInPath ? Environment.CurrentDirectory : Path.GetDirectoryName(file);
                if (string.IsNullOrWhiteSpace(commandLine)) commandLine = "";
                // 啟動信息
                ProcessStartInfo psi = new ProcessStartInfo
                {
                    UseShellExecute = true,
                    FileName = $"{file} {commandLine}", //解決帶參數的進程起不來或者起來的進程沒有參數的問題
                    Arguments = commandLine,
                    WorkingDirectory = workDir,
                    RedirectStandardError = false,
                    RedirectStandardOutput = false,
                    RedirectStandardInput = false,
                    CreateNoWindow = noWindow,
                    WindowStyle = minimize ? ProcessWindowStyle.Minimized : ProcessWindowStyle.Normal
                };
                // 在指定的用戶會話中創建進程
                SecurityAttributes saProcessAttributes = new SecurityAttributes();
                SecurityAttributes saThreadAttributes = new SecurityAttributes();
                CreateProcessFlags createProcessFlags = (noWindow ? CreateProcessFlags.CREATE_NO_WINDOW : CreateProcessFlags.CREATE_NEW_CONSOLE) | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT;
                bool success = CreateProcessAsUser(duplicateToken, null, $"{file} {commandLine}", ref saProcessAttributes, ref saThreadAttributes, false, createProcessFlags, environmentBlock, null, ref psi, out ProcessInformation pi);
                if (!success)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                    //throw new ApplicationException("Could not create process as user.");
                }
                return pi.dwProcessId;
            }
            finally
            {
                // 清理資源
                if (userToken != IntPtr.Zero) CloseHandle(userToken);
                if (duplicateToken != IntPtr.Zero) CloseHandle(duplicateToken);
                if (environmentBlock != IntPtr.Zero) DestroyEnvironmentBlock(environmentBlock);
            }
        }
        /// <summary>
        /// 使用win32api實現在指定用戶身份的環境變量中查找命令(command參數)是否存在。
        /// </summary>
        private static bool InPathOfSpecificUserEnvironment(in IntPtr userToken, in IntPtr environmentBlock, in string command)
        {
            // 在指定用戶會話環境中執行命令,并且獲得控制臺標準輸出內容
            string commandLine = $"cmd.exe /c chcp 65001 && where {command}";
            string output = ExecuteCommandAsUserAndReturnStdOutput(userToken, environmentBlock, commandLine, Encoding.UTF8);
            // OperatingSystem.IsOSPlatform("WINDOWS") 該方法僅在 .NET Core及以上版本可用,在 .NET Standard 和 .NET Framework 中不可用。
            // 現有操作系統中,Windows 操作系統的目錄分隔符為 '\',而 Unix 操作系統的目錄分隔符為 '/',因此可以用它來判斷和區分操作系統。
            // 如果是Windows操作系統,則不區分大小寫
            var comparison = Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
            return output.IndexOf(command, comparison) >= 0;
        }
        /// <summary>
        /// 在指定用戶會話環境中執行命令,并且返回控制臺標準輸出內容
        /// </summary>
        private static string ExecuteCommandAsUserAndReturnStdOutput(in IntPtr userToken, in IntPtr environmentBlock, string commandLine, Encoding encoding)
        {
            // 創建匿名管道
            var saPipeAttributes = new SecurityAttributes();
            saPipeAttributes.Length = Marshal.SizeOf(saPipeAttributes);
            saPipeAttributes.InheritHandle = true; // 允許句柄被繼承
                                                   //saPipeAttributes.SecurityDescriptor = IntPtr.Zero;
            if (!CreatePipe(out IntPtr readPipe, out IntPtr writePipe, ref saPipeAttributes, 0))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
            // 確保管道句柄有效
            if (readPipe == IntPtr.Zero)
            {
                throw new InvalidOperationException("Failed to create read pipe.");
            }
            if (writePipe == IntPtr.Zero)
            {
                throw new InvalidOperationException("Failed to create write pipe.");
            }
            try
            {
                // 確保讀取句柄不被子進程繼承
                SetHandleInformation(readPipe, 0x00000001/*HANDLE_FLAG_INHERIT*/, 0);
                var startInfo = new StartupInfo();
                startInfo.cb = Marshal.SizeOf(startInfo);
                // 設置子進程的標準輸出為管道的寫入端
                startInfo.hStdError = writePipe;
                startInfo.hStdOutput = writePipe;
                startInfo.dwFlags = StartupInfoFlags.STARTF_USESTDHANDLES;
                // 在用戶會話中創建進程
                const CreateProcessFlags createProcessFlags = CreateProcessFlags.CREATE_NEW_CONSOLE | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT;
                var success = CreateProcessAsUser(
                    userToken,
                    null,
                    commandLine,
                    ref saPipeAttributes,
                    ref saPipeAttributes,
                    true,
                    createProcessFlags,
                    environmentBlock,
                    null,
                    ref startInfo,
                    out ProcessInformation pi);
                if (!success)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
                // 關閉管道的寫入端句柄,因為它已經被子進程繼承
                CloseHandle(writePipe);
                writePipe = IntPtr.Zero;
                // 從管道的讀取端讀取數據
                string output;
                using (var streamReader = new StreamReader(new FileStream(new SafeFileHandle(readPipe, true), FileAccess.Read, 4096, false), encoding))
                {
                    // 讀取控制臺標準輸出內容
                    output = streamReader.ReadToEnd();
                    Trace.WriteLine($"The commandLine [{commandLine}] std output -> {output}");
                }
                // 關閉進程和線程句柄
                CloseHandle(pi.hProcess);
                CloseHandle(pi.hThread);
                // 返回控制臺標準輸出內容
                return output;
            }
            finally
            {
                if (readPipe != IntPtr.Zero) CloseHandle(readPipe);
                if (writePipe != IntPtr.Zero) CloseHandle(writePipe);
            }
        }
        /// <summary>
        /// 獲取活動會話的用戶訪問令牌
        /// </summary>
        /// <exception cref="Win32Exception"></exception>
        private static IntPtr GetSessionUserToken()
        {
            // 獲取當前活動的控制臺會話ID
            uint sessionId = WTSGetActiveConsoleSessionId();
            // 獲取活動會話的用戶訪問令牌
            bool success = WTSQueryUserToken(sessionId, out IntPtr hToken);
            // 如果失敗,則從會話列表中獲取第一個活動的會話ID,并再次嘗試獲取用戶訪問令牌
            if (!success)
            {
                sessionId = GetFirstActiveSessionOfEnumerateSessions();
                success = WTSQueryUserToken(sessionId, out hToken);
                if (!success)
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }
            return hToken;
        }
        /// <summary>
        /// 枚舉所有用戶會話,獲取第一個活動的會話ID
        /// </summary>
        private static uint GetFirstActiveSessionOfEnumerateSessions()
        {
            IntPtr pSessionInfo = IntPtr.Zero;
            try
            {
                int sessionCount = 0;
                // 枚舉所有用戶會話
                if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
                {
                    int arrayElementSize = Marshal.SizeOf(typeof(WtsSessionInfo));
                    IntPtr current = pSessionInfo;
                    for (int i = 0; i < sessionCount; i++)
                    {
                        WtsSessionInfo si = (WtsSessionInfo)Marshal.PtrToStructure(current, typeof(WtsSessionInfo));
                        current += arrayElementSize;
                        if (si.State == WtsConnectStateClass.WTSActive)
                        {
                            return si.SessionID;
                        }
                    }
                }
                return uint.MaxValue;
            }
            finally
            {
                WTSFreeMemory(pSessionInfo);
                CloseHandle(pSessionInfo);
            }
        }
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SecurityAttributes lpProcessAttributes, ref SecurityAttributes lpThreadAttributes, bool bInheritHandles, CreateProcessFlags dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref StartupInfo lpStartupInfo, out ProcessInformation lpProcessInformation);
        /// <summary>
        /// 以指定用戶的身份啟動進程
        /// </summary>
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            ref SecurityAttributes lpProcessAttributes,
            ref SecurityAttributes lpThreadAttributes,
            bool bInheritHandles,
            CreateProcessFlags dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref ProcessStartInfo lpStartupInfo,
            out ProcessInformation lpProcessInformation);
        /// <summary>
        /// 獲取當前活動的控制臺會話ID
        /// </summary>
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern uint WTSGetActiveConsoleSessionId();
        /// <summary>
        /// 枚舉所有用戶會話
        /// </summary>
        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern int WTSEnumerateSessions(IntPtr hServer, int reserved, int version, ref IntPtr ppSessionInfo, ref int pCount);
        /// <summary>
        /// 獲取活動會話的用戶訪問令牌
        /// </summary>
        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);
        /// <summary>
        /// 復制訪問令牌
        /// </summary>
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, ref SecurityAttributes lpTokenAttributes, SecurityImpersonationLevel impersonationLevel, TokenType tokenType, out IntPtr phNewToken);
        /// <summary>
        /// 創建環境塊(檢索指定用戶的環境)
        /// </summary>
        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
        /// <summary>
        /// 釋放環境塊
        /// </summary>
        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SecurityAttributes lpPipeAttributes, uint nSize);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags);
        [DllImport("wtsapi32.dll", SetLastError = false)]
        private static extern void WTSFreeMemory(IntPtr memory);
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hObject);
        [StructLayout(LayoutKind.Sequential)]
        private struct WtsSessionInfo
        {
            public readonly uint SessionID;
            [MarshalAs(UnmanagedType.LPStr)]
            public readonly string pWinStationName;
            public readonly WtsConnectStateClass State;
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct SecurityAttributes
        {
            public int Length;
            public IntPtr SecurityDescriptor;
            public bool InheritHandle;
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct StartupInfo
        {
            public int cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public StartupInfoFlags dwFlags;
            public UInt16 wShowWindow;
            public UInt16 cbReserved2;
            public unsafe byte* lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct ProcessInformation
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }
        private const uint TOKEN_DUPLICATE = 0x0002;
        private const uint MAXIMUM_ALLOWED = 0x2000000;
        /// <summary>
        /// Process Creation Flags。<br/>
        /// More:https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
        /// </summary>
        [Flags]
        private enum CreateProcessFlags : uint
        {
            DEBUG_PROCESS = 0x00000001,
            DEBUG_ONLY_THIS_PROCESS = 0x00000002,
            CREATE_SUSPENDED = 0x00000004,
            DETACHED_PROCESS = 0x00000008,
            /// <summary>
            /// The new process has a new console, instead of inheriting its parent's console (the default). For more information, see Creation of a Console. <br />
            /// This flag cannot be used with <see cref="DETACHED_PROCESS"/>.
            /// </summary>
            CREATE_NEW_CONSOLE = 0x00000010,
            NORMAL_PRIORITY_CLASS = 0x00000020,
            IDLE_PRIORITY_CLASS = 0x00000040,
            HIGH_PRIORITY_CLASS = 0x00000080,
            REALTIME_PRIORITY_CLASS = 0x00000100,
            CREATE_NEW_PROCESS_GROUP = 0x00000200,
            /// <summary>
            /// If this flag is set, the environment block pointed to by lpEnvironment uses Unicode characters. Otherwise, the environment block uses ANSI characters.
            /// </summary>
            CREATE_UNICODE_ENVIRONMENT = 0x00000400,
            CREATE_SEPARATE_WOW_VDM = 0x00000800,
            CREATE_SHARED_WOW_VDM = 0x00001000,
            CREATE_FORCEDOS = 0x00002000,
            BELOW_NORMAL_PRIORITY_CLASS = 0x00004000,
            ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000,
            INHERIT_PARENT_AFFINITY = 0x00010000,
            INHERIT_CALLER_PRIORITY = 0x00020000,
            CREATE_PROTECTED_PROCESS = 0x00040000,
            EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
            PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000,
            PROCESS_MODE_BACKGROUND_END = 0x00200000,
            CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
            CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
            CREATE_DEFAULT_ERROR_MODE = 0x04000000,
            /// <summary>
            /// The process is a console application that is being run without a console window. Therefore, the console handle for the application is not set. <br />
            /// This flag is ignored if the application is not a console application, or if it is used with either <see cref="CREATE_NEW_CONSOLE"/> or <see cref="DETACHED_PROCESS"/>.
            /// </summary>
            CREATE_NO_WINDOW = 0x08000000,
            PROFILE_USER = 0x10000000,
            PROFILE_KERNEL = 0x20000000,
            PROFILE_SERVER = 0x40000000,
            CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
        }
        /// <summary>
        /// 指定創建進程時的窗口工作站、桌面、標準句柄和main窗口的外觀。<br/>
        /// More:https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
        /// </summary>
        [Flags]
        private enum StartupInfoFlags : uint
        {
            /// <summary>
            /// 強制反饋光標顯示,即使用戶沒有啟用。
            /// </summary>
            STARTF_FORCEONFEEDBACK = 0x00000040,
            /// <summary>
            /// 強制反饋光標不顯示,即使用戶啟用了它。
            /// </summary>
            STARTF_FORCEOFFFEEDBACK = 0x00000080,
            /// <summary>
            /// 防止應用程序被固定在任務欄或開始菜單。
            /// </summary>
            STARTF_PREVENTPINNING = 0x00002000,
            /// <summary>
            /// 不再支持,原用于強制控制臺應用程序全屏運行。
            /// </summary>
            STARTF_RUNFULLSCREEN = 0x00000020,
            /// <summary>
            /// lpTitle成員是一個AppUserModelID。
            /// </summary>
            STARTF_TITLEISAPPID = 0x00001000,
            /// <summary>
            /// lpTitle成員是一個鏈接名。
            /// </summary>
            STARTF_TITLEISLINKNAME = 0x00000800,
            /// <summary>
            /// 啟動程序來自不受信任的源,可能會顯示警告。
            /// </summary>
            STARTF_UNTRUSTEDSOURCE = 0x00008000,
            /// <summary>
            /// 使用dwXCountChars和dwYCountChars成員。
            /// </summary>
            STARTF_USECOUNTCHARS = 0x00000008,
            /// <summary>
            /// 使用dwFillAttribute成員。
            /// </summary>
            STARTF_USEFILLATTRIBUTE = 0x00000010,
            /// <summary>
            /// 使用hStdInput成員指定熱鍵。
            /// </summary>
            STARTF_USEHOTKEY = 0x00000200,
            /// <summary>
            /// 使用dwX和dwY成員。
            /// </summary>
            STARTF_USEPOSITION = 0x00000004,
            /// <summary>
            /// 使用wShowWindow成員。
            /// </summary>
            STARTF_USESHOWWINDOW = 0x00000001,
            /// <summary>
            /// 使用dwXSize和dwYSize成員。
            /// </summary>
            STARTF_USESIZE = 0x00000002,
            /// <summary>
            /// 使用hStdInput、hStdOutput和hStdError成員。
            /// </summary>
            STARTF_USESTDHANDLES = 0x00000100
        }
        private enum WtsConnectStateClass
        {
            WTSActive,
            WTSConnected,
            WTSConnectQuery,
            WTSShadow,
            WTSDisconnected,
            WTSIdle,
            WTSListen,
            WTSReset,
            WTSDown,
            WTSInit
        }
        private enum SecurityImpersonationLevel
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        }
        private enum TokenType
        {
            TokenPrimary = 1,
            TokenImpersonation
        }
    }
}

用法:

ProcessUtils.StartProcessAsActiveUser("ping.exe", "www.baidu.com -t");
ProcessUtils.StartProcessAsActiveUser("notepad.exe");
ProcessUtils.StartProcessAsActiveUser("C:\\Windows\\System32\\notepad.exe");

在 Windows 7~11Windows Server 2016~2022 操作系統,測試通過。

轉自https://www.cnblogs.com/VAllen/p/18257879


該文章在 2025/5/17 10:03:00 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved