tjx
2025-10-11 75ff22c91d7710573231ba3ed75259f3d1477cc8
发送接口的底层调整
已修改4个文件
350 ■■■■ 文件已修改
WebApi/CLAUDE.md 215 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WebApi/Gs.HostIIS/appsettings.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WebApi/Gs.Sys/Services/FmController.cs 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WebApi/Gs.Toolbox/InterfaceUtil.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WebApi/CLAUDE.md
@@ -4,92 +4,189 @@
## Project Overview
This is a .NET 8 MES (Manufacturing Execution System) solution built with ASP.NET Core Web API. The system is organized as a modular monolith with domain-separated projects for different business areas.
This is a .NET 8 MES (Manufacturing Execution System) solution built with ASP.NET Core Web API. The system uses a custom API framework with modular architecture, organizing business functionality into domain-separated projects.
## Solution Structure
The solution follows a modular architecture with these main projects:
- **Gs.HostIIS**: Main Web API host application with Swagger documentation
- **Gs.Entity**: Data entities organized by business domains (BaseInfo, QC, Sys, Warehouse)
- **Gs.Toolbox**: Core utilities, API framework, and dependency injection infrastructure
- **Business Modules**:
  - **Gs.BaseInfo**: Basic information management (items, customers, suppliers, staff, etc.)
  - **Gs.Warehouse**: Inventory and warehouse management
  - **GS.QC**: Quality control functionality
- **Gs.HostIIS**: Main Web API host application (targets .NET 8.0)
- **Gs.Toolbox**: Core framework with custom DI, controller discovery, and utilities (targets .NET 6.0)
- **Gs.Entity**: Data entity models organized by domain (BaseInfo, QC, Sys, Warehouse)
- **Business Modules** (all target .NET 8.0):
  - **Gs.BaseInfo**: Basic information (items, customers, suppliers, staff, departments)
  - **Gs.Warehouse**: Inventory and warehouse operations
  - **GS.QC**: Quality control and inspection
  - **Gs.Sys**: System administration and user management
  - **Gs.Report**: Reporting functionality
  - **Gs.Wom**: Work order management
  - **Gs.Sales**: Sales management
  - **Gs.Pda**: Mobile/handheld device support
  - **Gs.QiTaCk** / **Gs.QiTaRk**: Additional business modules (其它出库/其它入库)
  - **Gs.QiTaCk** / **Gs.QiTaRk**: Miscellaneous in/out operations (其它出库/其它入库)
  - **Gs.Ww**: Outsourcing management (委外)
  - **Gs.JJGZ**: Piece-rate wage management (计件工资)
## Development Commands
### Build and Run
- Build entire solution: `dotnet build GsMesSolution.sln`
- Run main API: `dotnet run --project Gs.HostIIS`
- Build specific project: `dotnet build Gs.ProjectName/Gs.ProjectName.csproj`
```bash
# Build entire solution
dotnet build GsMesSolution.sln
# Run main API (starts on http://localhost:5263 by default)
dotnet run --project Gs.HostIIS
# Build specific module
dotnet build Gs.BaseInfo/Gs.BaseInfo.csproj
```
### Development Server
- The API runs on default ports (check launchSettings.json for specific ports)
- Swagger UI is available at `/swagger` endpoint
- CORS is configured to allow all origins for development
- API runs on `http://localhost:5263` (Project profile) or `http://localhost:37005` (IIS Express)
- Swagger UI: `http://localhost:5263/swagger`
- CORS configured to allow all origins in development
## Architecture Details
### Custom API Framework
The solution uses a custom API framework built in `Gs.Toolbox`:
- Custom dependency injection with lifecycle attributes (`ITransient`, `IScope`, `ISingleton`)
- Custom controller convention via `CustomApplicationModelConvention`
- API grouping for Swagger documentation via `ApiGroupAttribute`
- Custom authorization with `ApiAuthorizeAttribute`
The solution uses a custom framework in `Gs.Toolbox` that replaces standard ASP.NET Core conventions:
#### Controller Discovery
- Controllers are discovered by implementing the `IRomteService` interface (not by inheriting `ControllerBase`)
- `CustomControllerFeatureProvider` scans assemblies and the `Services` folder for classes implementing `IRomteService`
- Controllers are automatically registered without `[ApiController]` or `[Route]` attributes
- Business logic classes in `Services/` folders act as controllers
#### Routing Convention
- Routes automatically generated as `/{ControllerName}/{ActionName}` by `CustomApplicationModelConvention`
- HTTP methods specified via `[RequestMethod(RequestMethods.POST)]` attribute (not standard `[HttpPost]`)
- Parameter binding: non-primitive types automatically bound from request body for POST/PUT/PATCH
#### Dependency Injection
- Custom DI system using lifecycle marker interfaces:
  - `ITransient`: Transient lifetime (new instance per request)
  - `IScope`: Scoped lifetime (one instance per request)
  - `ISingleton`: Singleton lifetime (one instance for application)
- Classes implementing these interfaces are auto-registered
- Use `[Expose(typeof(IYourInterface))]` attribute to specify service interface explicitly
- Auto-injection scans all referenced assemblies via `builder.AddCustomInject()`
#### API Grouping
- Swagger groups defined via `[ApiGroup(ApiGroupNames.BaseInfo)]` attribute on controller classes
- Groups defined in `ApiGroupNames` enum (BaseInfo, QC, PerMission, WOM, ErpMes, etc.)
- Each group generates separate Swagger document for organization
### Data Access
- Uses SqlSugar ORM for database operations
- Connection string configured in `appsettings.json`
- Repository pattern implemented in `Gs.Toolbox/Repository.cs`
### Configuration
- Database: SQL Server (connection string in appsettings.json)
- External services: ERP integration endpoints configured
- File paths: Services, logs, upload, and download paths configured
- Custom JSON serialization with Newtonsoft.Json
#### SqlSugar ORM
- Base repository: `Repository<T>` in `Gs.Toolbox/Repository.cs`
- Connection string: `appsettings.json` → `ConnectionStrings` key
- Static `SqlSugarScope Db` instance provides database context
- Business classes inherit from `Repository<TEntity>` for data access
- Transaction support via `UseTransaction()` method
### Key Components
- **CustomContractResolver**: Custom JSON property naming (converts to camelCase)
- **ReturnDto<T>**: Standardized API response format with RtnCode, RtnData, RtnMsg
- **PageList<T>** and **PageQuery**: Pagination support
- **ExcelHelper**: Excel import/export functionality
- **LogHelper**: Logging utilities
- **DbHelperSQL**: Additional database utilities
#### Pagination
- Standard pagination using `PageQuery` (input) and `PageList<T>` (output)
- `PageQuery` fields: `currentPage`, `everyPageSize`, `sortName`, `sortOrder`, `keyWord`, `keyWhere`, `keyType`
- `CommonPage()` methods provide built-in pagination logic
### Framework Features
- Custom controller discovery via `CustomControllerFeatureProvider`
- Module system with `IModule` interface for extensibility
- Dependency injection container with custom lifecycle management
- API grouping system for organized Swagger documentation
### Response Format
## Business Domains
All API responses use `ReturnDto<T>` wrapper:
```csharp
{
  "rtnCode": 1,        // ReturnCode enum: Success=1, Default=-100, Unauthorized=-101, Exception=-102
  "rtnData": {...},    // Generic data payload
  "rtnMsg": "..."      // Optional message
}
```
The system handles typical MES functionality organized by API groups:
- Base information (BaseInfo): Items, customers, suppliers, departments, staff
- Quality control (QC): Inspection projects and quality management
- Warehouse (PerMission): Inventory and warehouse operations
- Work orders (WOM): Manufacturing execution
- ERP integration (ErpMes): Data exchange with ERP systems
- Reporting (Report/Rport): Various business reports
- Mobile support (PDA): Handheld device operations
- Additional modules: Sales (XS), Outsourcing (WW), etc.
### JSON Serialization
- Uses Newtonsoft.Json with custom `CustomContractResolver`
- Property naming: Converts to camelCase (first letter lowercase)
- Handles underscore-separated names (e.g., `USER_NAME` → `userName`)
- Date format: `"yyyy-MM-dd HH:mm:ss"`
- Reference loop handling: Ignore
### Authorization
- Custom `ApiAuthorizeAttribute` filter validates `token` header
- Token format: `"token {guid}"` in request header
- Use `[AllowAnonymous]` to bypass authorization
- User context extraction via `UtilityHelper.GetUserGuidAndOrgGuid(IHttpContextAccessor)`
### Configuration (appsettings.json)
- `ConnectionStrings`: SQL Server connection string
- `TestErpUrl`, `TestErpUrl2`, `ProductionErpUrl`: ERP integration endpoints
- `ServicesPath`: Path to compiled DLLs for controller discovery (default: "Services")
- `LogPath`: Log file directory (default: "logs")
- `UploadPath`: File upload directory (mapped to `/upload` endpoint)
- `DownPath`: File download directory (mapped to `/down` endpoint)
### Project Output Configuration
Business modules use custom output paths:
- Compiled DLLs output to `Gs.HostIIS/bin/Debug/` (BaseOutputPath configured in .csproj)
- XML documentation files generated automatically for Swagger
- Framework scans `Services/` folder at runtime for controller DLLs
## Creating New Business Modules
1. **Create new class library project** targeting .NET 8.0
2. **Reference required projects**:
   ```xml
   <ProjectReference Include="..\Gs.Entity\Gs.Entity.csproj" />
   <ProjectReference Include="..\Gs.Toolbox\Gs.Toolbox.csproj" />
   ```
3. **Configure output path** in .csproj:
   ```xml
   <BaseOutputPath>..\Gs.HostIIS\bin</BaseOutputPath>
   <OutputPath>..\Gs.HostIIS\bin\Debug\</OutputPath>
   <GenerateDocumentationFile>True</GenerateDocumentationFile>
   ```
4. **Create Services folder** for controller classes
5. **Implement controller** by inheriting `Repository<TEntity>` and `IRomteService`:
   ```csharp
   [ApiGroup(ApiGroupNames.YourGroup)]
   public class YourManager : Repository<YourEntity>, IRomteService
   {
       [RequestMethod(RequestMethods.POST)]
       public ReturnDto<PageList<YourEntity>> GetListPage(PageQuery query) { ... }
   }
   ```
6. **Add to solution** and build (DLL auto-discovered at runtime)
## Common Patterns
### Creating API Endpoints
```csharp
[ApiGroup(ApiGroupNames.BaseInfo)]
public class MesItemsManager : Repository<MesItems>, IRomteService
{
    private readonly IHttpContextAccessor _http;
    public MesItemsManager(IHttpContextAccessor httpContextAccessor)
    {
        _http = httpContextAccessor;
    }
    [RequestMethod(RequestMethods.POST)]
    public ReturnDto<PageList<MesItems>> GetListPage(PageQuery query)
    {
        // Business logic using Db property from Repository<T>
    }
}
```
### Using Transactions
```csharp
var affectedRows = UseTransaction(db => {
    db.Insertable(entity).ExecuteCommand();
    db.Updateable(otherEntity).ExecuteCommand();
    return db.Ado.AffectedRows;
});
```
## Development Notes
- All projects target .NET 8.0 (except Gs.Toolbox which targets .NET 6.0)
- Nullable reference types are enabled
- The system uses a Chinese interface (comments and some naming in Chinese)
- Custom API framework provides dependency injection and controller conventions
- Swagger documentation is automatically generated with XML comments from Services folder
- Response format follows ReturnDto pattern with standardized error codes
- File upload/download paths are configured and served as static files
- The system uses Chinese for business domain comments and naming
- Gs.Toolbox targets .NET 6.0 for compatibility; all other projects target .NET 8.0
- Controllers are discovered at runtime from `Services/` folder - no need to manually register
- Swagger automatically includes all controllers with their XML documentation
- Static files (uploads/downloads) served from paths configured in appsettings.json
WebApi/Gs.HostIIS/appsettings.json
@@ -6,11 +6,10 @@
    }
  },
  "AllowedHosts": "*",
  /*"ConnectionStrings": "Data Source=192.168.1.146;Initial Catalog=TEST_MES;User ID=testUser;Password =qixi1qaz@WSXtest",*/
  "ConnectionStrings": "Data Source=192.168.0.51;Initial Catalog=TEST_MES;User ID=sa;Password =LanBao@2025;Encrypt=True;TrustServerCertificate=True;",
  "TestErpUrl": "http://192.168.1.149:8066/WebService1.asmx/MesToErpinfoTest",
  "TestErpUrl2": "http://192.168.1.149:8066/WebService1.asmx/MesToErpUpdateFlag",
  "ProductionErpUrl": "http://192.168.1.149:8066/WebService1.asmx/mesToErpinfoFormal",
  "TestErpUrl": "http://192.168.0.52:8055/",
  "TestErpUrl2": "http://192.168.0.52:8055/",
  "ProductionErpUrl": "http://192.168.0.52:8055/",
  "ServicesPath": "Services",
  "LogPath": "logs",
  "UploadPath": "upload",
WebApi/Gs.Sys/Services/FmController.cs
@@ -28,25 +28,29 @@
                GetUserGuidAndOrgGuid(_http);
        }
        #region 版面
        #region 布局配置
        /// <summary>
        ///     增加
        ///     保存或清空表单布局
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        /// <remarks>Saves layouts: intType 1=standard save, 2=personal save, 3=clear standard, 4=clear personal.</remarks>
        [RequestMethod(RequestMethods.POST)]
        public ReturnDto<ExpandoObject> EditModel([FromBody] dynamic model)
        {
            string applyUserGuid = "";
            string formPath = model.formPath;
            int intType = model.intType;
            // intType: 1 = save standard layout, 2 = save personal layout, 3 = clear standard layout, 4 = clear personal layout.
            dynamic m = new ExpandoObject();
            m.outMsg = "";
            // Gather batched SQL statements so they can be executed transactionally when saving layouts.
            Hashtable SQLStringList = new Hashtable();
            string _groupGuid = Guid.NewGuid().ToString();
            //只有超级管理员权限
            if (intType == 1 || intType == 3)
            {
                // Validate the current user has administrator rights when touching standard layouts.
                int? isAdmin = 0;
                try
                {
@@ -65,6 +69,7 @@
            //保存标准版
            if (intType == 1)
            {
                // Persist a new standard layout definition shared by all users.
                applyUserGuid = null;
                Gs.Toolbox.DbHelperSQL.ExecuteSql("delete from [FM_LAYOUT] where groupGuid<>'" + _groupGuid + "' and [formPath]=@formPath and applyUserGuid is null", new SqlParameter[] { new SqlParameter("@formPath", formPath) });
                // SQLStringList.Add("delete from [FM_LAYOUT] where groupGuid<>'" + _groupGuid + "' and [formPath]=@formPath and applyUserGuid is null", new SqlParameter[] { new SqlParameter("@formPath", formPath) });
@@ -72,6 +77,7 @@
            //保存个人版本
            if (intType == 2)
            {
                // Persist the caller's personal layout copy scoped to their user GUID.
                applyUserGuid = _userGuid;
                Gs.Toolbox.DbHelperSQL.ExecuteSql("delete from [FM_LAYOUT] where  groupGuid<>'" + _groupGuid + "' and [formPath]=@formPath and applyUserGuid =@applyUserGuid", new SqlParameter[] { new SqlParameter("@formPath", formPath), new SqlParameter("@applyUserGuid", applyUserGuid) });
                //SQLStringList.Add("delete from [FM_LAYOUT] where  groupGuid<>'" + _groupGuid + "' and [formPath]=@formPath and applyUserGuid =@applyUserGuid", new SqlParameter[] { new SqlParameter("@formPath", formPath), new SqlParameter("@applyUserGuid", applyUserGuid) });
@@ -79,6 +85,7 @@
            //清空标准版本
            if (intType == 3)
            {
                // Administrators can wipe the shared standard layout entirely.
                applyUserGuid = null;
                SQLStringList.Add("delete from [FM_LAYOUT] where [formPath]=@formPath and applyUserGuid is null", new SqlParameter[] { new SqlParameter("@formPath", formPath) });
                Gs.Toolbox.DbHelperSQL.ExecuteSqlTranRtn(SQLStringList);
@@ -88,6 +95,7 @@
            //清空个人版本
            if (intType == 4)
            {
                // Remove the caller's personal layout while keeping the shared standard version intact.
                applyUserGuid = _userGuid;
                SQLStringList.Add("delete from [FM_LAYOUT] where [formPath]=@formPath and applyUserGuid =@applyUserGuid", new SqlParameter[] { new SqlParameter("@formPath", formPath), new SqlParameter("@applyUserGuid", applyUserGuid) });
                Gs.Toolbox.DbHelperSQL.ExecuteSqlTranRtn(SQLStringList);
@@ -103,6 +111,7 @@
                    JObject job = (JObject)jsonitem;
                    if (job["idName"] != null)
                    {
                        // Compose an insert statement for each UI control (grid, layout panel, splitter, etc.).
                        string idName = job["idName"].ToString();
                        string idXml = job["idXml"].ToString();
                        string idType = job["idType"].ToString();
@@ -127,6 +136,7 @@
            }
            catch (Exception ex)
            {
                // 捕获保存查询配置时的异常,并将信息返回给前端。
                m.outMsg = "操作失败:" + ex.Message;
                Gs.Toolbox.LogHelper.Debug(this.ToString(), "EditModel error:" + ex.Message);
            }
@@ -140,6 +150,7 @@
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        /// <remarks>Combines the shared layout (list) with the current user's override (list2).</remarks>
        [RequestMethod(RequestMethods.POST)]
        public ReturnDto<ExpandoObject> GetModel([FromBody] dynamic model)
        {
@@ -155,10 +166,12 @@
            var dset = new DataSet();
            try
            {
                // Stored procedure returns both standard layout data and any personal override for the current user.
                dset = DbHelperSQL.RunProcedure("[fm_get_layout]", parameters, "0");
                if (dset != null && dset.Tables.Count > 0
                 )
                {
                    // Table[0] represents the standard definition; table[1] holds the user's personal layout snapshot.
                    var _tb = dset.Tables[0].TableToDynamicList();
                    m.list = _tb;
                    var _tb2 = dset.Tables[1].TableToDynamicList();
@@ -167,6 +180,7 @@
            }
            catch (Exception ex)
            {
                // Log retrieval failure but continue returning default result to caller.
                LogHelper.Debug(ToString(), ex.Message);
            }
            if (m != null)
@@ -182,6 +196,7 @@
        /// </summary>
        /// <param name="guid"></param>
        /// <returns></returns>
        /// <remarks>Retrieves the serialized layout string for the latest saved version (standard or personal).</remarks>
        [RequestMethod(RequestMethods.POST)]
        public ReturnDto<string> GetModelByVersion([FromBody] dynamic model)
        {
@@ -195,6 +210,7 @@
            var dset = new DataSet();
            try
            {
                // Stored procedure exposes the latest serialized layout snapshot based on formPath and user scope.
                dset = DbHelperSQL.RunProcedure("[fm_get_layout_ver]", parameters, "0");
                if (dset != null && dset.Tables.Count > 0
                 )
@@ -204,6 +220,7 @@
            }
            catch (Exception ex)
            {
                // Capture context when reading layout versions fails to help diagnose environment-specific issues.
                LogHelper.Debug(ToString(), ex.Message+ ",formPath:"+ formPath+ ",_userGuid:"+ _userGuid);
            }
@@ -217,6 +234,7 @@
        {
            int? isAdmin = 0;
            System.Text.StringBuilder _sb = new System.Text.StringBuilder();
            // Uses SYS_USER.IS_SYS flag to decide if the caller has elevated privileges.
            _sb.Append("select count(1) from [dbo].[SYS_USER] where GUID='" + _userGuid + "' and  IS_SYS=1");
            object _obj = Gs.Toolbox.DbHelperSQL.GetSingle(_sb.ToString());
            if (_obj == null)
@@ -235,6 +253,7 @@
        /// </summary>
        /// <param name="model">keyType:1审核,0反审核</param>
        /// <returns></returns>
        /// <remarks>Packages MES data into ERP payloads and posts them according to the requested operation.</remarks>
        [RequestMethod(RequestMethods.POST)]
        public string SendErp([FromBody] dynamic model)
        {
@@ -245,6 +264,7 @@
            //string keyChild = model.keyChild;任务子节点名
            //string keyMeth = model.keyMeth;方法名
            //string keyNo = model.keyNo;单据编号
            //string keyUrl = model.keyUrl;接口地址
            int _rtnInt = 0;
            string _rtnStr = "";
            try
@@ -256,13 +276,21 @@
                string keyGuid = model.keyGuid;
                string keyNo = model.keyNo;
                string idtype = model.idtype;//这个仅仅是更新工单状态的时候有
                string keyUrl = model.keyUrl;
                if (string.IsNullOrEmpty(idtype))
                    (_rtnInt, _rtnStr) = InterfaceUtil.HttpPostErp(_erpJson, keyUserGuid, keyGuid, keyNo);
                {
                    // 常规接口:按操作类型推送单条业务数据。
                    (_rtnInt, _rtnStr) = InterfaceUtil.HttpPostErp(_erpJson, keyUserGuid, keyGuid, keyNo,0,keyUrl);
                }
                else
                    (_rtnInt, _rtnStr) = InterfaceUtil.HttpPostErp(_erpJson, keyUserGuid, keyGuid, keyNo, 2);
                {
                    // 带 idtype 的请求用于特殊流程(如关闭、反关闭),ERP 需要额外的状态标记。
                    (_rtnInt, _rtnStr) = InterfaceUtil.HttpPostErp(_erpJson, keyUserGuid, keyGuid, keyNo, 2,keyUrl);
                }
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                Gs.Toolbox.LogHelper.Debug(this.ToString(), "Fm SendErp:" + ex.Message);
                return "发送erp失败:" + ex.Message;
            }
@@ -289,6 +317,7 @@
            string keyNo = model.keyNo;
            string idtype = model.idtype;//这个仅仅是更新工单状态的时候有
            if (keyMeth.ToUpper() == "delete".ToUpper())
                // 删除操作无需向 ERP 推送数据,只需返回空串。
                return "";
            try
            {
@@ -299,6 +328,7 @@
                       new("@inEdtUserGuid", keyUserGuid),
                       new("@keyMeth", keyMeth.ToLower()),
                   };
                // 调用业务定义的存储过程,将 MES 数据打包给 ERP。
                dset = DbHelperSQL.RunProcedure(keyProduce, parameters, "0");
                if (dset == null)
                    return "";
@@ -309,10 +339,12 @@
                //这是普通的接口
                if (string.IsNullOrEmpty(idtype))
                {
                    // 常规出参:第一张表是主数据,第二张表(若存在)是子表集合。
                    string _mesGuid = dset.Tables[0].Rows[0][0].ToString();
                    dynamic _datajson = new ExpandoObject();
                    if (dset.Tables.Count > 1)
                    {
                        // 多表返回时,需要把子表集合挂到 datajson 中。
                        //这是这是普通的接口里的结案,结构和其它不一样
                        if (keyMeth.ToLower() == "toclose".ToLower() || keyMeth.ToLower() == "closure".ToLower() || keyMeth.ToLower() == "unfinish")
                        {
@@ -325,30 +357,33 @@
                            ((IDictionary<string, object>)_datajson)[keyChild] = _lst;
                        }
                    }
                    var _obj = new
                    {
                        mesid = _mesGuid,
                        taskname = keyTaskName,
                        optype = keyMeth,
                        datajson = JsonConvert.SerializeObject(_datajson),
                    };
                    return JsonConvert.SerializeObject(_obj);
                    // var _obj = new
                    // {
                    //     mesid = _mesGuid,
                    //     taskname = keyTaskName,
                    //     optype = keyMeth,
                    //     datajson = JsonConvert.SerializeObject(_datajson),
                    // };
                    // return JsonConvert.SerializeObject(_obj);
                    return JsonConvert.SerializeObject(_datajson);
                }
                //这是订单回传标识
                List<dynamic> _datajson22 = new List<dynamic>();
                dynamic _ob = new ExpandoObject();
                _ob.ENTRY = dset.Tables[0].TableToDynamicList();
                _datajson22.Add(_ob);
                var _obj22 = new
                {
                    taskname = keyTaskName,
                    idtype = idtype,
                    datajson = JsonConvert.SerializeObject(_datajson22),
                };
                return JsonConvert.SerializeObject(_obj22);
                // var _obj22 = new
                // {
                //     taskname = keyTaskName,
                //     idtype = idtype,
                //     datajson = JsonConvert.SerializeObject(_datajson22),
                // };
                // return JsonConvert.SerializeObject(_obj22);
                return JsonConvert.SerializeObject(_datajson22);
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                Gs.Toolbox.LogHelper.Debug(this.ToString(), ex.Message);
                throw ex;
            }
@@ -386,6 +421,7 @@
            }
            ;
            var lst = new List<dynamic>();
            // 将列名和显示标题拼成 "~" 分隔的参数,传给存储过程生成查询配置。
            SqlParameter[] parameters =
            {
                new("@formPath", formPath),
@@ -394,6 +430,7 @@
            var dset = new DataSet();
            try
            {
                // fm_set_query 会返回查询条件、结果字段、排序等多张配置表。
                dset = DbHelperSQL.RunProcedure("[fm_set_query]", parameters, "0");
                if (dset != null && dset.Tables.Count > 0)
                {
@@ -405,6 +442,7 @@
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                LogHelper.Debug(ToString(), ex.Message);
            }
            return ReturnDto<dynamic>.QuickReturn(m, ReturnCode.Success, "读取成功!");
@@ -429,16 +467,19 @@
                isAdmin = chkAdmin();
                if (isAdmin <= 0)
                {
                    // Query configuration is restricted to administrators to protect shared metadata.
                    m.outMsg = "你不是管理员,操作失败!";
                    return ReturnDto<dynamic>.QuickReturn(m, ReturnCode.Default, "操作成功!");
                }
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                Gs.Toolbox.LogHelper.Debug(this.ToString(), "EditModel isAdmin error:" + ex.Message);
            }
            try
            {
                // 先清空原有查询来源表,再批量插入最新配置。
                Gs.Toolbox.DbHelperSQL.ExecuteSql("delete from [FM_QUERY_TABLE] where formPath=@formPath ", new SqlParameter[] { new SqlParameter("@formPath", formPath) });
                foreach (var _obj in model.list)
                {
@@ -451,6 +492,7 @@
            }
            catch (Exception ex)
            {
                // 捕获保存查询配置时的异常,并将信息返回给前端。
                m.outMsg = ex.Message;
                return ReturnDto<dynamic>.QuickReturn(m, ReturnCode.Default, ex.Message);
            }
@@ -473,15 +515,18 @@
                isAdmin = chkAdmin();
                if (isAdmin <= 0)
                {
                    // 删除查询配置同样需要管理员权限。
                    return ReturnDto<int>.QuickReturn(rtnInt, ReturnCode.Default, "你不是管理员,操作失败!");
                }
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                Gs.Toolbox.LogHelper.Debug(this.ToString(), "EditModel isAdmin error:" + ex.Message);
            }
            Guid? guid = model.guid;
            System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
            // 采用 GUID 精确删除指定的查询数据源记录。
            stringBuilder.Append("delete from FM_QUERY_TABLE where guid='" + guid + "'");
            rtnInt = Gs.Toolbox.DbHelperSQL.ExecuteSql(stringBuilder.ToString());
            if (rtnInt <= 0)
@@ -504,11 +549,13 @@
                isAdmin = chkAdmin();
                if (isAdmin <= 0)
                {
                    // 只有管理员才能调整查询列字段映射。
                    return ReturnDto<int>.QuickReturn(rtnInt, ReturnCode.Default, "你不是管理员,操作失败!");
                }
            }
            catch (Exception ex)
            {
                // 记录 ERP 数据转换异常,便于定位存储过程或序列化问题。
                Gs.Toolbox.LogHelper.Debug(this.ToString(), "EditModel isAdmin error:" + ex.Message);
            }
            Guid? guid = model.guid;
WebApi/Gs.Toolbox/InterfaceUtil.cs
@@ -18,16 +18,18 @@
    /// <param name="hNo"></param>
    /// <param name="urlType">如果为2,则是更新工单状态</param>
    /// <returns>如果成功返回日志guid,否则返回串</returns>
    public static (int, string) HttpPostErp(string param, string edtUserGuid = "", string abtGuid = "", string hNo = "", int urlType = 0)
    public static (int, string) HttpPostErp(string param,
        string edtUserGuid = "", string abtGuid = "", string hNo = "",
        int urlType = 0, string keyUrl = "")
    {
        int _rtn = 0;
        //日志详细,发送的时候,记录日志,存储过程调用的时候,再累加上mes业务的操作结果
        System.Text.StringBuilder sbLog = new System.Text.StringBuilder();
        sbLog.Append(DateTime.Now.ToString() + "开始发送");
        string strLogGuid = Guid.NewGuid().ToString();
        string url = AppSettingsHelper.getValueByKey("TestErpUrl");
        string url = AppSettingsHelper.getValueByKey("TestErpUrl") + keyUrl;
        if (urlType == 2)
            url = AppSettingsHelper.getValueByKey("TestErpUrl2");
            url = AppSettingsHelper.getValueByKey("TestErpUrl2") + keyUrl;
        HttpWebRequest request = null;
        StreamWriter requestStream = null;
        WebResponse response = null;
@@ -41,7 +43,9 @@
            request.Timeout = 150000;
            request.AllowAutoRedirect = false;
            request.ServicePoint.Expect100Continue = false;
            HttpRequestCachePolicy noCachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
            HttpRequestCachePolicy noCachePolicy =
                new HttpRequestCachePolicy(HttpRequestCacheLevel
                    .NoCacheNoStore);
            request.CachePolicy = noCachePolicy;
            requestStream = new StreamWriter(request.GetRequestStream());
            requestStream.Write(param);
@@ -57,7 +61,8 @@
        }
        catch (Exception ex)
        {
            LogHelper.Debug(url, "HttpPostErp response:" + param + ",ex:" + ex.Message);
            LogHelper.Debug(url,
                "HttpPostErp response:" + param + ",ex:" + ex.Message);
            responseStr = ex.Message;
            _rtn = -1;
        }
@@ -67,12 +72,14 @@
            requestStream = null;
            response = null;
        }
        if (_rtn != -1)
        {
            Result _result = JsonConvert.DeserializeObject<Result>(responseStr);
            if ("200".Equals(_result.state))
                _rtn = 1;
        }
        sbLog.Append("," + DateTime.Now.ToString() + "结束发送");
        if (_rtn > 0)
            sbLog.Append(",发送成功");
@@ -81,24 +88,25 @@
        try
        {
            SqlParameter[] parameters =
    {
            new("@edtUserGuid", edtUserGuid),
            new("@abtGuid", abtGuid),
            new("@abtTable", ""),
            new("@detail", sbLog.ToString()),
            new("@hNo", hNo),
            new("@RtnLogGuid", strLogGuid),
            new("@SendJson", param),
            new("@RtnJson", responseStr),
            new("@isSuccess", (_rtn>0?1:0)),
            new("@isErp", 1),
        };
            {
                new("@edtUserGuid", edtUserGuid),
                new("@abtGuid", abtGuid),
                new("@abtTable", ""),
                new("@detail", sbLog.ToString()),
                new("@hNo", hNo),
                new("@RtnLogGuid", strLogGuid),
                new("@SendJson", param),
                new("@RtnJson", responseStr),
                new("@isSuccess", (_rtn > 0 ? 1 : 0)),
                new("@isErp", 1),
            };
            DbHelperSQL.RunProcedure("[prc_log_create]", parameters);
        }
        catch (Exception ex)
        {
            LogHelper.Debug(url, "HttpPostErp 写入日志表" + ex.Message);
        }
        return (_rtn, (_rtn > 0 ? strLogGuid : responseStr));
    }
}
@@ -109,6 +117,7 @@
    /// 200成功,否则失败
    /// </summary>
    public string? state { get; set; }
    public string? msg { get; set; }
    public string? status { get; set; }