Просмотр исходного кода

feat(s5): 进出存查询只读查询接口 + 前端接线

S5z 库存数据/进出存查询:新增 InoutQuery 只读查询(list/loc-options/transtype-options),忠实复刻证据 sql.txt(InvTransHist 库存交易流水 join GeneralizedCodeMaster 交易类型名/LocationMaster 货位名/ItemMaster 物料名·单位);进出存口径 SQL 内定义(期初=BeginBalance,入库=QtyChange>0,出库=QtyChange<0,期末=BeginBalance+QtyChange);前端既有骨架接线(接口/货位·交易类型下拉/日期筛/分页/交易时间格式化/空态),未改列结构。数据源 aidopdev 只读,无 DDL/无写路径。

chore: bump version Web 2.4.217 / server 1.0.222
YY968XX 6 дней назад
Родитель
Сommit
334d191d8e

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "admin.net",
   "type": "module",
-  "version": "2.4.216",
+  "version": "2.4.217",
   "packageManager": "pnpm@10.32.1",
   "lastBuildTime": "2026.03.15",
   "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 71 - 0
Web/src/views/aidop/s5/api/inoutQuery.ts

@@ -0,0 +1,71 @@
+import service from '/@/utils/request';
+import { withAidopTenantParams } from '../../api/aidopTenant';
+
+export interface InoutQueryPaged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+// ── S5 进出存查询(只读,数据源 aidopdev.InvTransHist 库存交易流水,sql.txt 口径)──
+export interface InoutQueryRow {
+	/** 交易时间 CreateTime */
+	transTime?: string;
+	/** 物料编码 ItemNum */
+	materialCode?: string;
+	/** 物料名称 ItemMaster.Descr */
+	materialName?: string;
+	/** 货位 Loc */
+	location?: string;
+	/** 货位名称 LocationMaster.Descr */
+	locationName?: string;
+	/** 批次号 LotSerial */
+	batchNo?: string;
+	/** 期初数 BeginBalance */
+	beginQty?: number;
+	/** 入库数 QtyChange>0?QtyChange:0 */
+	inQty?: number;
+	/** 出库数 QtyChange<0?abs(QtyChange):0 */
+	outQty?: number;
+	/** 期末数 BeginBalance+QtyChange */
+	endQty?: number;
+	/** 单位 UM */
+	unit?: string;
+	/** 交易类型(名称,TransType.Comments) */
+	transType?: string;
+	/** 工单号 WorkOrd */
+	workOrderNo?: string;
+	/** 发货单号 ShipperNum */
+	deliveryNo?: string;
+	/** 发货类型 ShipType */
+	deliveryType?: string;
+	/** 备注 Remark */
+	remark?: string;
+	/** 创建用户 CreateUser */
+	createUser?: string;
+}
+
+export interface InoutQueryLocationOption {
+	val?: string;
+	label?: string;
+}
+
+export interface InoutQueryTransTypeOption {
+	val?: string;
+	label?: string;
+}
+
+export function fetchInoutQueryList(params: any) {
+	return service
+		.get<InoutQueryPaged<InoutQueryRow>>('/api/S5InoutQuery/list', { params: withAidopTenantParams(params) })
+		.then((r) => r.data);
+}
+
+export function fetchInoutQueryLocationOptions() {
+	return service.get<InoutQueryLocationOption[]>('/api/S5InoutQuery/loc-options').then((r) => r.data);
+}
+
+export function fetchInoutQueryTransTypeOptions() {
+	return service.get<InoutQueryTransTypeOption[]>('/api/S5InoutQuery/transtype-options').then((r) => r.data);
+}

+ 75 - 7
Web/src/views/aidop/s5/inventory/inoutQueryList.vue

@@ -5,7 +5,9 @@
 				<el-input v-model="query.materialCode" clearable style="width: 200px" />
 			</el-form-item>
 			<el-form-item label="货位">
-				<el-select v-model="query.location" placeholder="请选择" clearable style="width: 200px" />
+				<el-select v-model="query.location" placeholder="请选择" clearable filterable style="width: 200px">
+					<el-option v-for="o in locationOptions" :key="o.val" :label="o.label ? `${o.val} ${o.label}` : o.val" :value="o.val!" />
+				</el-select>
 			</el-form-item>
 			<el-form-item label="批次号">
 				<el-input v-model="query.batchNo" clearable style="width: 200px" />
@@ -14,7 +16,9 @@
 				<el-input v-model="query.workOrderNo" clearable style="width: 200px" />
 			</el-form-item>
 			<el-form-item label="交易类型">
-				<el-select v-model="query.transType" placeholder="请选择" clearable style="width: 200px" />
+				<el-select v-model="query.transType" placeholder="请选择" clearable filterable style="width: 200px">
+					<el-option v-for="o in transTypeOptions" :key="o.val" :label="o.label || o.val" :value="o.val!" />
+				</el-select>
 			</el-form-item>
 			<el-form-item label="创建时间">
 				<el-date-picker v-model="query.createTime" type="date" value-format="YYYY-MM-DD" style="width: 180px" />
@@ -28,8 +32,10 @@
 			</el-form-item>
 		</el-form>
 
-		<el-table :data="rows" row-key="id" v-loading="loading" border stripe>
-			<el-table-column prop="transTime" label="交易时间" min-width="170" resizable />
+		<el-table :data="rows" v-loading="loading" border stripe>
+			<el-table-column prop="transTime" label="交易时间" min-width="170" resizable>
+				<template #default="{ row }">{{ fmtDateTime(row.transTime) }}</template>
+			</el-table-column>
 			<el-table-column prop="materialCode" label="物料编码" min-width="140" show-overflow-tooltip resizable />
 			<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip resizable />
 			<el-table-column prop="location" label="货位" min-width="110" show-overflow-tooltip resizable />
@@ -58,15 +64,26 @@
 				:total="total"
 				:page-sizes="[10, 20, 50]"
 				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
 			/>
 		</div>
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopS5InventoryInoutQuery">
-import { computed, reactive, ref } from 'vue';
+import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import {
+	fetchInoutQueryList,
+	fetchInoutQueryLocationOptions,
+	fetchInoutQueryTransTypeOptions,
+	type InoutQueryRow,
+	type InoutQueryLocationOption,
+	type InoutQueryTransTypeOption,
+} from '../api/inoutQuery';
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '进出存查询');
@@ -83,13 +100,57 @@ const query = reactive({
 	pageSize: 10,
 });
 
-// 本批次为只读 UI 骨架,尚无真实进出存接口:渲染空表,不发请求、不造数据。
 const loading = ref(false);
-const rows = ref<Record<string, unknown>[]>([]);
+const rows = ref<InoutQueryRow[]>([]);
 const total = ref(0);
+const locationOptions = ref<InoutQueryLocationOption[]>([]);
+const transTypeOptions = ref<InoutQueryTransTypeOption[]>([]);
+
+function fmtDateTime(v?: string | null) {
+	if (!v) return '';
+	const s = String(v);
+	return s.length > 19 ? s.slice(0, 19).replace('T', ' ') : s.replace('T', ' ');
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchInoutQueryList({
+			materialCode: query.materialCode,
+			location: query.location,
+			batchNo: query.batchNo,
+			workOrderNo: query.workOrderNo,
+			transType: query.transType,
+			createTime: query.createTime,
+			createUser: query.createUser,
+			page: query.page,
+			pageSize: query.pageSize,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} catch (e: any) {
+		rows.value = [];
+		total.value = 0;
+		ElMessage.error(e?.message || '加载进出存查询列表失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
+async function loadOptions() {
+	try {
+		const [loc, tt] = await Promise.all([fetchInoutQueryLocationOptions(), fetchInoutQueryTransTypeOptions()]);
+		locationOptions.value = loc || [];
+		transTypeOptions.value = tt || [];
+	} catch {
+		locationOptions.value = [];
+		transTypeOptions.value = [];
+	}
+}
 
 function doSearch() {
 	query.page = 1;
+	loadList();
 }
 
 function resetQuery() {
@@ -101,7 +162,14 @@ function resetQuery() {
 	query.createTime = '';
 	query.createUser = '';
 	query.page = 1;
+	loadList();
 }
+
+onMounted(() => {
+	loadOptions();
+	loadList();
+});
+onActivated(() => loadList());
 </script>
 
 <style scoped lang="scss">

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET ͨ��Ȩ�޿���ƽ̨</Description>
-    <AssemblyVersion>1.0.221</AssemblyVersion>
-    <FileVersion>1.0.221</FileVersion>
-    <Version>1.0.221</Version>
+    <AssemblyVersion>1.0.222</AssemblyVersion>
+    <FileVersion>1.0.222</FileVersion>
+    <Version>1.0.222</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 121 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/Dto/InoutQueryDto.cs

@@ -0,0 +1,121 @@
+namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
+
+/// <summary>
+/// S5 进出存查询 列表查询入参(只读)。
+///
+/// 数据源:aidopdev.InvTransHist(库存交易流水),按证据 sql.txt 口径 join
+/// GeneralizedCodeMaster(交易类型名)/LocationMaster(货位名)/ItemMaster(物料名/单位)。
+/// 进出存口径由 SQL 内定义:期初=BeginBalance;入库=QtyChange&gt;0?QtyChange:0;
+/// 出库=QtyChange&lt;0?abs(QtyChange):0;期末=BeginBalance+QtyChange。
+/// Domain 由 GeneralizedCodeMaster(SystemConfig/CompanyCode) 解析,默认 8010。
+/// InvTransHist 无 tenant_id;TenantId 仅为前端 withAidopTenantParams 注入的透传参数,本查询不据其过滤。
+/// </summary>
+public class InoutQueryListInput
+{
+    /// <summary>页码(从 1 开始)</summary>
+    public int Page { get; set; } = 1;
+
+    /// <summary>每页条数</summary>
+    public int PageSize { get; set; } = 10;
+
+    /// <summary>物料编码(InvTransHist.ItemNum,模糊匹配)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>货位(InvTransHist.Loc,精确匹配,下拉选择)</summary>
+    public string? Location { get; set; }
+
+    /// <summary>批次号(InvTransHist.LotSerial,模糊匹配)</summary>
+    public string? BatchNo { get; set; }
+
+    /// <summary>工单号(InvTransHist.WorkOrd,模糊匹配)</summary>
+    public string? WorkOrderNo { get; set; }
+
+    /// <summary>交易类型(InvTransHist.TransType 代码,精确匹配,下拉选择)</summary>
+    public string? TransType { get; set; }
+
+    /// <summary>创建时间(InvTransHist.CreateTime 当日,yyyy-MM-dd)</summary>
+    public string? CreateTime { get; set; }
+
+    /// <summary>创建用户(InvTransHist.CreateUser,模糊匹配)</summary>
+    public string? CreateUser { get; set; }
+
+    /// <summary>租户 ID(前端 withAidopTenantParams 注入;InvTransHist 无租户列,本查询忽略)</summary>
+    public long? TenantId { get; set; }
+}
+
+/// <summary>
+/// S5 进出存查询 列表行(只读)。字段对齐前端列 prop(camelCase),源列见注释(sql.txt / InvTransHist)。
+/// </summary>
+public class InoutQueryListRow
+{
+    /// <summary>交易时间(InvTransHist.CreateTime)</summary>
+    public DateTime? TransTime { get; set; }
+
+    /// <summary>物料编码(InvTransHist.ItemNum)</summary>
+    public string? MaterialCode { get; set; }
+
+    /// <summary>物料名称(ItemMaster.Descr)</summary>
+    public string? MaterialName { get; set; }
+
+    /// <summary>货位(InvTransHist.Loc)</summary>
+    public string? Location { get; set; }
+
+    /// <summary>货位名称(LocationMaster.Descr)</summary>
+    public string? LocationName { get; set; }
+
+    /// <summary>批次号(InvTransHist.LotSerial)</summary>
+    public string? BatchNo { get; set; }
+
+    /// <summary>期初数(InvTransHist.BeginBalance)</summary>
+    public decimal? BeginQty { get; set; }
+
+    /// <summary>入库数(QtyChange&gt;0?QtyChange:0)</summary>
+    public decimal? InQty { get; set; }
+
+    /// <summary>出库数(QtyChange&lt;0?abs(QtyChange):0)</summary>
+    public decimal? OutQty { get; set; }
+
+    /// <summary>期末数(BeginBalance+QtyChange)</summary>
+    public decimal? EndQty { get; set; }
+
+    /// <summary>单位(ItemMaster.UM)</summary>
+    public string? Unit { get; set; }
+
+    /// <summary>交易类型(GeneralizedCodeMaster TransType.Comments 名称,无解码回落代码)</summary>
+    public string? TransType { get; set; }
+
+    /// <summary>工单号(InvTransHist.WorkOrd)</summary>
+    public string? WorkOrderNo { get; set; }
+
+    /// <summary>发货单号(InvTransHist.ShipperNum)</summary>
+    public string? DeliveryNo { get; set; }
+
+    /// <summary>发货类型(InvTransHist.ShipType)</summary>
+    public string? DeliveryType { get; set; }
+
+    /// <summary>备注(InvTransHist.Remark)</summary>
+    public string? Remark { get; set; }
+
+    /// <summary>创建用户(InvTransHist.CreateUser)</summary>
+    public string? CreateUser { get; set; }
+}
+
+/// <summary>货位下拉选项(LocationMaster.Location + Descr)。</summary>
+public class InoutQueryLocationOption
+{
+    /// <summary>货位(LocationMaster.Location)</summary>
+    public string? Val { get; set; }
+
+    /// <summary>货位名称(LocationMaster.Descr)</summary>
+    public string? Label { get; set; }
+}
+
+/// <summary>交易类型下拉选项(GeneralizedCodeMaster TransType:Val 代码 + Comments 名称)。</summary>
+public class InoutQueryTransTypeOption
+{
+    /// <summary>交易类型代码(GeneralizedCodeMaster.Val)</summary>
+    public string? Val { get; set; }
+
+    /// <summary>交易类型名称(GeneralizedCodeMaster.Comments)</summary>
+    public string? Label { get; set; }
+}

+ 166 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/MaterialWarehouse/InoutQueryService.cs

@@ -0,0 +1,166 @@
+using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
+
+namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
+
+/// <summary>
+/// S5 进出存查询 只读 list 服务。
+///
+/// 数据源:aidopdev.InvTransHist(库存交易流水),忠实复刻证据 sql.txt:
+/// LEFT JOIN GeneralizedCodeMaster(TransType→名称)、LocationMaster(货位名)、ItemMaster(物料名/单位),
+/// 过滤 a.Domain=@Domain。进出存口径由 SQL 内定义(非脑补):
+///   期初 = BeginBalance;入库 = QtyChange&gt;0?QtyChange:0;出库 = QtyChange&lt;0?abs(QtyChange):0;
+///   期末(结存) = BeginBalance+QtyChange。逐笔交易流水,BeginBalance 按行存储。
+/// Domain 由 GeneralizedCodeMaster(SystemConfig/CompanyCode) 解析,默认 8010。
+/// 排序 a.RecID DESC(PK,近似交易时间倒序,兼顾 1M 行分页性能与稳定)。
+///
+/// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转;InvTransHist 无 tenant_id,不做租户过滤。
+/// </summary>
+[ApiDescriptionSettings(Order = 303, Description = "进出存查询")]
+[Route("api/S5InoutQuery")]
+[AllowAnonymous]
+[NonUnify]
+public class InoutQueryService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    public InoutQueryService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    /// <summary>解析公司域(GeneralizedCodeMaster SystemConfig/CompanyCode,默认 8010)。</summary>
+    private async Task<string> ResolveDomainAsync()
+    {
+        var list = await _db.Ado.SqlQueryAsync<string>(
+            "SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig' AND Val='CompanyCode' ORDER BY RecID LIMIT 1");
+        var domain = list.FirstOrDefault();
+        return string.IsNullOrWhiteSpace(domain) ? "8010" : domain;
+    }
+
+    /// <summary>
+    /// 进出存查询列表(只读分页查询)。
+    /// </summary>
+    [DisplayName("进出存查询列表")]
+    [HttpGet("list")]
+    public async Task<object> GetList([FromQuery] InoutQueryListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var domain = await ResolveDomainAsync();
+
+        var where = new List<string> { "a.Domain = @Domain" };
+        var pars = new List<SugarParameter> { new("@Domain", domain) };
+
+        if (!string.IsNullOrWhiteSpace(input.MaterialCode))
+        {
+            where.Add("a.ItemNum LIKE @MaterialCode");
+            pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Location))
+        {
+            where.Add("a.Loc = @Location");
+            pars.Add(new SugarParameter("@Location", input.Location.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.BatchNo))
+        {
+            where.Add("a.LotSerial LIKE @BatchNo");
+            pars.Add(new SugarParameter("@BatchNo", $"%{input.BatchNo.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.WorkOrderNo))
+        {
+            where.Add("a.WorkOrd LIKE @WorkOrderNo");
+            pars.Add(new SugarParameter("@WorkOrderNo", $"%{input.WorkOrderNo.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.TransType))
+        {
+            where.Add("a.TransType = @TransType");
+            pars.Add(new SugarParameter("@TransType", input.TransType.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.CreateTime))
+        {
+            where.Add("a.CreateTime >= @CreateFrom AND a.CreateTime < DATE_ADD(@CreateFrom, INTERVAL 1 DAY)");
+            pars.Add(new SugarParameter("@CreateFrom", $"{input.CreateTime.Trim()} 00:00:00"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.CreateUser))
+        {
+            where.Add("a.CreateUser LIKE @CreateUser");
+            pars.Add(new SugarParameter("@CreateUser", $"%{input.CreateUser.Trim()}%"));
+        }
+
+        var whereSql = string.Join(" AND ", where);
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM InvTransHist a WHERE {whereSql}", pars);
+
+        var list = await _db.Ado.SqlQueryAsync<InoutQueryListRow>(
+            $"""
+            SELECT
+                a.CreateTime  AS TransTime,
+                a.ItemNum     AS MaterialCode,
+                i.Descr       AS MaterialName,
+                a.Loc         AS Location,
+                IFNULL(l.descr,'') AS LocationName,
+                a.LotSerial   AS BatchNo,
+                a.BeginBalance AS BeginQty,
+                (CASE WHEN a.QtyChange > 0 THEN a.QtyChange ELSE 0 END) AS InQty,
+                (CASE WHEN a.QtyChange < 0 THEN ABS(a.QtyChange) ELSE 0 END) AS OutQty,
+                (a.BeginBalance + a.QtyChange) AS EndQty,
+                i.UM          AS Unit,
+                COALESCE(b.Comments, a.TransType) AS TransType,
+                a.WorkOrd     AS WorkOrderNo,
+                a.ShipperNum  AS DeliveryNo,
+                a.ShipType    AS DeliveryType,
+                a.Remark      AS Remark,
+                a.CreateUser  AS CreateUser
+            FROM InvTransHist a
+            LEFT JOIN GeneralizedCodeMaster b ON a.Domain = b.Domain AND a.TransType = b.Val AND b.FldName = 'TransType'
+            LEFT JOIN LocationMaster l ON a.Domain = l.Domain AND a.Loc = l.location
+            LEFT JOIN ItemMaster i ON a.Domain = i.Domain AND a.ItemNum = i.ItemNum
+            WHERE {whereSql}
+            ORDER BY a.RecID DESC
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    /// <summary>
+    /// 货位下拉选项(LocationMaster.Location + Descr,只读)。
+    /// </summary>
+    [DisplayName("进出存查询货位选项")]
+    [HttpGet("loc-options")]
+    public async Task<object> GetLocationOptions()
+    {
+        var domain = await ResolveDomainAsync();
+        var list = await _db.Ado.SqlQueryAsync<InoutQueryLocationOption>(
+            """
+            SELECT location AS Val, descr AS Label
+            FROM LocationMaster
+            WHERE Domain = @Domain AND IFNULL(location,'') <> ''
+            ORDER BY location
+            """,
+            new List<SugarParameter> { new("@Domain", domain) });
+        return list;
+    }
+
+    /// <summary>
+    /// 交易类型下拉选项(GeneralizedCodeMaster TransType:代码 + 名称,只读)。
+    /// </summary>
+    [DisplayName("进出存查询交易类型选项")]
+    [HttpGet("transtype-options")]
+    public async Task<object> GetTransTypeOptions()
+    {
+        var domain = await ResolveDomainAsync();
+        var list = await _db.Ado.SqlQueryAsync<InoutQueryTransTypeOption>(
+            """
+            SELECT Val AS Val, Comments AS Label
+            FROM GeneralizedCodeMaster
+            WHERE FldName = 'TransType' AND Domain = @Domain AND IFNULL(Val,'') <> ''
+            ORDER BY Val
+            """,
+            new List<SugarParameter> { new("@Domain", domain) });
+        return list;
+    }
+}