From b503d19ef6aa148bb95a0f51fcb1fed32a349864 Mon Sep 17 00:00:00 2001 From: Swanky <413564165@qq.com> Date: Sat, 13 Jun 2026 23:07:31 +0800 Subject: [PATCH] 1 --- .gitignore | 7 + data/magic-api/api/大屏数据/group.json | 14 + .../交易中心实时服务信息.ms | 78 ++++ .../交易所牦牛成交数据.ms | 125 +++++++ .../api/大屏数据/地图迁徙数据.ms | 217 +++++++++++ .../api/大屏数据/实时交易统计.ms | 116 ++++++ .../api/大屏数据/市场实时监控.ms | 171 +++++++++ .../市场实时监控播放配置.ms | 77 ++++ .../api/大屏数据/市场环境监控.ms | 95 +++++ .../大屏数据/活牛鲜肉价格趋势.ms | 116 ++++++ .../大屏数据/牦牛供应实时信息.ms | 173 +++++++++ .../api/大屏数据/牦牛供应详情.ms | 344 ++++++++++++++++++ .../大屏数据/牦牛销售类型统计.ms | 73 ++++ .../api/大屏数据/系统配置.ms | 74 ++++ .../api/大屏数据/综合销售统计.ms | 157 ++++++++ .../大屏数据/采购商户来源分析.ms | 55 +++ data/magic-api/api/测试分组/group.json | 14 + .../api/测试分组/测试接口.ms | 25 ++ magic-api-dashboard-interfaces.txt | 234 ++++++++++++ pom.xml | 59 +++ .../trading/LivestockTradingApplication.java | 12 + src/main/resources/application.yml | 33 ++ .../LivestockTradingApplicationTests.java | 12 + 23 files changed, 2281 insertions(+) create mode 100644 .gitignore create mode 100644 data/magic-api/api/大屏数据/group.json create mode 100644 data/magic-api/api/大屏数据/交易中心实时服务信息.ms create mode 100644 data/magic-api/api/大屏数据/交易所牦牛成交数据.ms create mode 100644 data/magic-api/api/大屏数据/地图迁徙数据.ms create mode 100644 data/magic-api/api/大屏数据/实时交易统计.ms create mode 100644 data/magic-api/api/大屏数据/市场实时监控.ms create mode 100644 data/magic-api/api/大屏数据/市场实时监控播放配置.ms create mode 100644 data/magic-api/api/大屏数据/市场环境监控.ms create mode 100644 data/magic-api/api/大屏数据/活牛鲜肉价格趋势.ms create mode 100644 data/magic-api/api/大屏数据/牦牛供应实时信息.ms create mode 100644 data/magic-api/api/大屏数据/牦牛供应详情.ms create mode 100644 data/magic-api/api/大屏数据/牦牛销售类型统计.ms create mode 100644 data/magic-api/api/大屏数据/系统配置.ms create mode 100644 data/magic-api/api/大屏数据/综合销售统计.ms create mode 100644 data/magic-api/api/大屏数据/采购商户来源分析.ms create mode 100644 data/magic-api/api/测试分组/group.json create mode 100644 data/magic-api/api/测试分组/测试接口.ms create mode 100644 magic-api-dashboard-interfaces.txt create mode 100644 pom.xml create mode 100644 src/main/java/com/livestock/trading/LivestockTradingApplication.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/livestock/trading/LivestockTradingApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8640df6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +.idea/ +*.iml +.classpath +.project +.settings/ +.DS_Store diff --git a/data/magic-api/api/大屏数据/group.json b/data/magic-api/api/大屏数据/group.json new file mode 100644 index 0000000..293d34e --- /dev/null +++ b/data/magic-api/api/大屏数据/group.json @@ -0,0 +1,14 @@ +{ + "properties" : { }, + "id" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "大屏数据", + "type" : "api", + "parentId" : "0", + "path" : "/dashboard", + "createTime" : 1780876800000, + "updateTime" : null, + "createBy" : "admin", + "updateBy" : null, + "paths" : [ ], + "options" : [ ] +} diff --git a/data/magic-api/api/大屏数据/交易中心实时服务信息.ms b/data/magic-api/api/大屏数据/交易中心实时服务信息.ms new file mode 100644 index 0000000..96f2813 --- /dev/null +++ b/data/magic-api/api/大屏数据/交易中心实时服务信息.ms @@ -0,0 +1,78 @@ +{ + "properties" : { }, + "id" : "6f7a8490b1c2d3e4f5a678904ab", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "交易中心实时服务信息", + "createTime" : 1780877300000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/exchange-service-info", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "交易中心实时服务信息:牦牛供应/待售/已售、剩余车位、进场车辆、供应商数量(只读)。牦牛=yak_trade_ear_tag_inventory;车位=yak_car_parking_zone;车辆=yak_car_record+yak_trade_entry_record;供应商=yak_sn_customer(SELLER)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +var sql = """ +SELECT + (SELECT COUNT(*) + FROM yak_trade_ear_tag_inventory) AS total_supply, + (SELECT COUNT(*) + FROM yak_trade_ear_tag_inventory + WHERE status IN ('AVAILABLE', 'LOCKED')) AS for_sale_yaks, + (SELECT COUNT(*) + FROM yak_trade_ear_tag_inventory + WHERE status = 'SOLD') AS sold_yaks, + (SELECT COALESCE(SUM(total_capacity - current_count), 0) + FROM yak_car_parking_zone + WHERE status = 'NORMAL') AS remaining_parking, + (SELECT COUNT(*) + FROM ( + SELECT plate_no AS vehicle_no + FROM yak_car_record + WHERE del_flag = '0' + AND entry_time >= CURRENT_DATE + UNION + SELECT vehicle_no + FROM yak_trade_entry_record + WHERE entered_at >= CURRENT_DATE + ) v) AS entering_vehicles, + (SELECT COUNT(*) + FROM yak_sn_customer + WHERE del_flag = '0' + AND customer_type = 'SELLER') AS supplier_count +""" + +var rows = db.select(sql) +var row = rows && rows.length > 0 ? rows[0] : null + +if (!row) { + return { + totalSupply: 0, + forSaleYaks: 0, + soldYaks: 0, + remainingParking: 0, + enteringVehicles: 0, + supplierCount: 0 + } +} + +return { + totalSupply: row.totalSupply ? row.totalSupply : 0, + forSaleYaks: row.forSaleYaks ? row.forSaleYaks : 0, + soldYaks: row.soldYaks ? row.soldYaks : 0, + remainingParking: row.remainingParking ? row.remainingParking : 0, + enteringVehicles: row.enteringVehicles ? row.enteringVehicles : 0, + supplierCount: row.supplierCount ? row.supplierCount : 0 +} diff --git a/data/magic-api/api/大屏数据/交易所牦牛成交数据.ms b/data/magic-api/api/大屏数据/交易所牦牛成交数据.ms new file mode 100644 index 0000000..33195b4 --- /dev/null +++ b/data/magic-api/api/大屏数据/交易所牦牛成交数据.ms @@ -0,0 +1,125 @@ +{ + "properties" : { }, + "id" : "3c4d5e6f7a8490b1c2d3e4f5a678901", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "交易所牦牛成交数据", + "createTime" : 1780877000000, + "updateTime" : 1780817847278, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/yak-trading-data", + "method" : "GET", + "parameters" : [ { + "name" : "period", + "value" : null, + "description" : "时间维度:day(近6日)/week(近6周)/month(近6月),不传则返回全部", + "required" : false, + "dataType" : "String", + "type" : null, + "defaultValue" : null, + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + } ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : "{\n \"code\": -1,\n \"message\": \"系统内部出现错误\",\n \"data\": null,\n \"timestamp\": 1780817836665,\n \"executeTime\": 4\n}", + "description" : "交易所牦牛成交数据:近6个时间段的牦牛交易数量(头)与成交订单数量(单)趋势,数据源 yak_sn_order 已完成订单(只读查询)", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ + +var buildSeries = (rows) => { + var labels = [] + var yakTradingVolume = [] + var orderCount = [] + + for (row in rows) { + labels.push(row.label) + yakTradingVolume.push(row.yakVolume ? row.yakVolume : 0) + orderCount.push(row.orderCount ? row.orderCount : 0) + } + + return { + labels: labels, + yakTradingVolume: yakTradingVolume, + orderCount: orderCount + } +} + +var daySql = """ +SELECT + TO_CHAR(d.bucket_date, 'FMMM/FMDD') AS label, + COALESCE(SUM(o.quantity), 0) AS yak_volume, + COUNT(o.id) AS order_count +FROM ( + SELECT (CURRENT_DATE - offs)::date AS bucket_date, offs + FROM generate_series(5, 0, -1) AS offs +) d +LEFT JOIN yak_sn_order o + ON o.del_flag = '0' + AND o.status = 'COMPLETED' + AND o.transaction_time >= d.bucket_date + AND o.transaction_time < d.bucket_date + INTERVAL '1 day' +GROUP BY d.bucket_date, d.offs +ORDER BY d.offs DESC +""" + +var weekSql = """ +SELECT + TO_CHAR(d.anchor_date, 'FMMM/FMDD') || '-' || TO_CHAR(d.anchor_date + 6, 'FMMM/FMDD') AS label, + COALESCE(SUM(o.quantity), 0) AS yak_volume, + COUNT(o.id) AS order_count +FROM ( + SELECT (CURRENT_DATE - offs * 7)::date AS anchor_date, offs + FROM generate_series(5, 0, -1) AS offs +) d +LEFT JOIN yak_sn_order o + ON o.del_flag = '0' + AND o.status = 'COMPLETED' + AND o.transaction_time >= d.anchor_date + AND o.transaction_time < d.anchor_date + INTERVAL '7 days' +GROUP BY d.anchor_date, d.offs +ORDER BY d.offs DESC +""" + +var monthSql = """ +SELECT + TO_CHAR(d.month_start, 'YYYY/FMMM') AS label, + COALESCE(SUM(o.quantity), 0) AS yak_volume, + COUNT(o.id) AS order_count +FROM ( + SELECT (date_trunc('month', CURRENT_DATE) - (offs || ' months')::interval)::date AS month_start, offs + FROM generate_series(5, 0, -1) AS offs +) d +LEFT JOIN yak_sn_order o + ON o.del_flag = '0' + AND o.status = 'COMPLETED' + AND o.transaction_time >= d.month_start + AND o.transaction_time < d.month_start + INTERVAL '1 month' +GROUP BY d.month_start, d.offs +ORDER BY d.offs DESC +""" + +var dayRows = db.select(daySql) +var weekRows = db.select(weekSql) +var monthRows = db.select(monthSql) + +var result = { + day: buildSeries(dayRows), + week: buildSeries(weekRows), + month: buildSeries(monthRows) +} + +var p = period +if (p && result[p]) { + return result[p] +} + +return result diff --git a/data/magic-api/api/大屏数据/地图迁徙数据.ms b/data/magic-api/api/大屏数据/地图迁徙数据.ms new file mode 100644 index 0000000..24ae4d5 --- /dev/null +++ b/data/magic-api/api/大屏数据/地图迁徙数据.ms @@ -0,0 +1,217 @@ +{ + "properties" : { }, + "id" : "9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "地图迁徙数据", + "createTime" : 1780882000000, + "updateTime" : 1781360000000, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/map-trading-network", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "中央地图迁徙数据:销售网络/源地供应/红原本地出栏,数据源 yak_sn_order + yak_sn_customer(只读)。枢纽名称需与系统配置 mapHub.name 一致。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读统计已完成订单;hubName 需与系统配置 mapHub.name 保持一致 + +var hubName = '红原县' + +var getRowText = (row, camelKey, snakeKey) => { + if (row[camelKey]) { + return '' + row[camelKey] + } + if (row[snakeKey]) { + return '' + row[snakeKey] + } + return '' +} + +var getRowNumber = (row, key) => { + var v = row[key] + if (v == null || v === '') { + return 0 + } + return v +} + +var normalizeCityKey = (name) => { + var text = '' + (name == null ? '' : name) + if (text.trim() == '') { + return '未知' + } + + if (text.indexOf('北京') >= 0) { return '北京' } + if (text.indexOf('上海') >= 0) { return '上海' } + if (text.indexOf('天津') >= 0) { return '天津' } + if (text.indexOf('重庆') >= 0) { return '重庆' } + if (text.indexOf('成都') >= 0) { return '成都' } + if (text.indexOf('拉萨') >= 0) { return '拉萨' } + if (text.indexOf('西宁') >= 0) { return '西宁' } + if (text.indexOf('兰州') >= 0) { return '兰州' } + if (text.indexOf('西安') >= 0) { return '西安' } + if (text.indexOf('昆明') >= 0) { return '昆明' } + if (text.indexOf('贵阳') >= 0) { return '贵阳' } + if (text.indexOf('广州') >= 0) { return '广州' } + if (text.indexOf('深圳') >= 0) { return '深圳' } + if (text.indexOf('康定') >= 0) { return '康定' } + if (text.indexOf('香格里拉') >= 0) { return '香格里拉' } + if (text.indexOf('合作') >= 0) { return '合作' } + if (text.indexOf('甘南') >= 0) { return '甘南藏族自治州' } + if (text.indexOf('玉树') >= 0) { return '玉树' } + if (text.indexOf('甘孜') >= 0) { return '甘孜藏族自治州' } + if (text.indexOf('阿坝') >= 0) { return '阿坝县' } + if (text.indexOf('马尔康') >= 0) { return '马尔康' } + if (text.indexOf('理塘') >= 0) { return '理塘' } + if (text.indexOf('那曲') >= 0) { return '那曲' } + if (text.indexOf('果洛') >= 0) { return '果洛' } + if (text.indexOf('红原') >= 0) { return '红原县' } + + return text +} + +var buildDirectionalFlows = (rows, direction) => { + var flows = [] + var valueMap = {} + var descMap = {} + + if (!rows) { + return flows + } + + for (row in rows) { + var rawName = getRowText(row, 'placeName', 'place_name') + var value = getRowNumber(row, 'value') + var sample = getRowText(row, 'samplePlace', 'sample_place') + if (sample == '') { + sample = rawName + } + + if (value > 0) { + var cityKey = direction == 'local' ? rawName : normalizeCityKey(rawName) + if (cityKey != '未知') { + if (valueMap[cityKey] == null) { + valueMap[cityKey] = value + descMap[cityKey] = sample + if (direction == 'outflow') { + flows.push({ + from: hubName, + to: cityKey, + value: value, + description: sample + }) + } else { + flows.push({ + from: cityKey, + to: hubName, + value: value, + description: sample + }) + } + } else { + valueMap[cityKey] = valueMap[cityKey] + value + var descText = '' + descMap[cityKey] + if (descText.indexOf(sample) < 0 && sample != '') { + descMap[cityKey] = descText + ';' + sample + } + for (flow in flows) { + var flowCity = direction == 'outflow' ? flow.to : flow.from + if (flowCity == cityKey) { + flow.value = valueMap[cityKey] + flow.description = descMap[cityKey] + } + } + } + } + } + } + + return flows +} + +var outflowSql = """ +SELECT + COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.destination_place), ''), '未知') AS place_name, + COALESCE(SUM(o.quantity), 0) AS value, + MAX(COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.destination_place), ''), '未知')) AS sample_place +FROM yak_sn_order o +LEFT JOIN yak_sn_customer c ON c.id = o.buyer_id AND c.del_flag = '0' +WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' +GROUP BY COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.destination_place), ''), '未知') +HAVING COALESCE(SUM(o.quantity), 0) > 0 +ORDER BY value DESC, place_name +""" + +var inflowSql = """ +SELECT + COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.origin_place), ''), '未知') AS place_name, + COALESCE(SUM(o.quantity), 0) AS value, + MAX(COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.origin_place), ''), '未知')) AS sample_place +FROM yak_sn_order o +LEFT JOIN yak_sn_customer c ON c.id = o.seller_id AND c.del_flag = '0' +WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' +GROUP BY COALESCE(NULLIF(TRIM(c.region_name), ''), NULLIF(TRIM(o.origin_place), ''), '未知') +HAVING COALESCE(SUM(o.quantity), 0) > 0 +ORDER BY value DESC, place_name +""" + +var localSql = """ +SELECT + COALESCE( + NULLIF((regexp_match(o.origin_place, '([^省市区县]+(?:镇|乡|街道))'))[1], ''), + NULLIF(TRIM(o.origin_place), ''), + '未知' + ) AS place_name, + COALESCE(SUM(o.quantity), 0) AS value, + MAX(o.origin_place) AS sample_place +FROM yak_sn_order o +WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' + AND position('红原' in o.origin_place) > 0 +GROUP BY COALESCE( + NULLIF((regexp_match(o.origin_place, '([^省市区县]+(?:镇|乡|街道))'))[1], ''), + NULLIF(TRIM(o.origin_place), ''), + '未知' + ) +HAVING COALESCE(SUM(o.quantity), 0) > 0 +ORDER BY value DESC, place_name +""" + +var outflowRows = db.select(outflowSql) +var inflowRows = db.select(inflowSql) +var localRows = db.select(localSql) + +var outflowFlows = buildDirectionalFlows(outflowRows, 'outflow') +var inflowFlows = buildDirectionalFlows(inflowRows, 'inflow') +var localFlows = buildDirectionalFlows(localRows, 'local') + +return { + tradingModes: { + outflow: { + title: '销售网络分布图', + description: '从' + hubName + '向全国各地输出牦牛的流向分布', + flows: outflowFlows + }, + inflow: { + title: '源地供应分布图', + description: '全国各地向' + hubName + '供应牦牛的来源分布', + flows: inflowFlows + }, + local: { + title: '红原出栏分布图', + description: hubName + '各乡镇牦牛出栏分布情况', + flows: localFlows + } + } +} diff --git a/data/magic-api/api/大屏数据/实时交易统计.ms b/data/magic-api/api/大屏数据/实时交易统计.ms new file mode 100644 index 0000000..0a26799 --- /dev/null +++ b/data/magic-api/api/大屏数据/实时交易统计.ms @@ -0,0 +1,116 @@ +{ + "properties" : { }, + "id" : "2b3c4d5e6f748590a1b2c3d4e5f6789", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "实时交易统计", + "createTime" : 1780876800000, + "updateTime" : 1780803706100, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/real-time-stats", + "method" : "GET", + "parameters" : [ { + "name" : "dimension", + "value" : null, + "description" : "时间维度:day(当天)/week(近一周)/month(近一月)/year(当年),不传则返回全部", + "required" : false, + "dataType" : "String", + "type" : null, + "defaultValue" : null, + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + } ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "实时交易统计:牦牛交易总量、订单交易总量、销售商户数量、采购商户数量,按当天/近一周/近一月/当年统计(只读查询)", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读统计已完成订单,不修改任何数据 +var sql = """ +SELECT + dim, + COALESCE(SUM(quantity), 0) AS yak_total_volume, + COUNT(*) AS order_total_volume, + COUNT(DISTINCT seller_id) AS seller_count, + COUNT(DISTINCT buyer_id) AS buyer_count +FROM ( + SELECT 'day' AS dim, quantity, seller_id, buyer_id + FROM yak_sn_order + WHERE del_flag = '0' + AND status = 'COMPLETED' + AND transaction_time >= CURRENT_DATE + AND transaction_time < CURRENT_DATE + INTERVAL '1 day' + + UNION ALL + + SELECT 'week' AS dim, quantity, seller_id, buyer_id + FROM yak_sn_order + WHERE del_flag = '0' + AND status = 'COMPLETED' + AND transaction_time >= CURRENT_DATE - INTERVAL '6 days' + AND transaction_time < CURRENT_DATE + INTERVAL '1 day' + + UNION ALL + + SELECT 'month' AS dim, quantity, seller_id, buyer_id + FROM yak_sn_order + WHERE del_flag = '0' + AND status = 'COMPLETED' + AND transaction_time >= CURRENT_DATE - INTERVAL '29 days' + AND transaction_time < CURRENT_DATE + INTERVAL '1 day' + + UNION ALL + + SELECT 'year' AS dim, quantity, seller_id, buyer_id + FROM yak_sn_order + WHERE del_flag = '0' + AND status = 'COMPLETED' + AND transaction_time >= DATE_TRUNC('year', CURRENT_DATE) + AND transaction_time < CURRENT_DATE + INTERVAL '1 day' +) t +GROUP BY dim +ORDER BY dim +""" + +var rows = db.select(sql) + +var createEmptyStats = () => { + return { + yakTotalVolume: 0, + orderTotalVolume: 0, + sellerCount: 0, + buyerCount: 0 + } +} + +var result = { + day: createEmptyStats(), + week: createEmptyStats(), + month: createEmptyStats(), + year: createEmptyStats() +} + +for (row in rows) { + result[row.dim] = { + yakTotalVolume: row.yakTotalVolume, + orderTotalVolume: row.orderTotalVolume, + sellerCount: row.sellerCount, + buyerCount: row.buyerCount + } +} + +var dim = dimension +if (dim && result[dim]) { + return result[dim] +} + +return result diff --git a/data/magic-api/api/大屏数据/市场实时监控.ms b/data/magic-api/api/大屏数据/市场实时监控.ms new file mode 100644 index 0000000..d16ff20 --- /dev/null +++ b/data/magic-api/api/大屏数据/市场实时监控.ms @@ -0,0 +1,171 @@ +{ + "properties" : { }, + "id" : "c3d4e5f678901234567890abcdef0123", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "市场实时监控", + "createTime" : 1781199600000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/market-realtime-monitor", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "市场实时监控:视频监控设备列表,数据源 iot_device_video + iot_device_video_data 最新一条(只读)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +var sql = """ +SELECT + v.id, + v.name, + v.number, + v.location, + v.address, + v.status, + v.play_url, + v.hd_play_url, + v.preview_img_url, + v.img, + v.channel_number, + v."index" AS sort_index, + v.last_capture_time, + d.stream_url AS latest_stream_url, + d.snapshot_url AS latest_snapshot_url, + d.online_status AS latest_online_status, + d.stream_status AS latest_stream_status, + d.fault_code AS latest_fault_code +FROM iot_device_video v +LEFT JOIN LATERAL ( + SELECT + stream_url, + snapshot_url, + online_status, + stream_status, + fault_code + FROM iot_device_video_data + WHERE device_id = v.id + ORDER BY collect_time DESC NULLS LAST + LIMIT 1 +) d ON true +WHERE COALESCE(v.del_flag, '0') = '0' + AND COALESCE(v.is_show, true) = true +ORDER BY v."index" NULLS LAST, v.name +""" + +var rows = db.select(sql) +var cameras = [] + +var pickText = (row, camelKey, snakeKey) => { + if (!row) { + return '' + } + if (row[camelKey]) { + return row[camelKey] + } + if (row[snakeKey]) { + return row[snakeKey] + } + return '' +} + +var toUpperText = (text) => { + if (!text) { + return '' + } + return ('' + text).toUpperCase() +} + +var mapStatus = (row) => { + var deviceStatus = pickText(row, 'status', 'status') + var onlineStatus = pickText(row, 'latestOnlineStatus', 'latest_online_status') + var faultCode = pickText(row, 'latestFaultCode', 'latest_fault_code') + var deviceUpper = toUpperText(deviceStatus) + + if (deviceUpper === 'OFFLINE') { + return 'offline' + } + if (deviceUpper === 'ERROR' || deviceUpper === 'FAULT') { + return 'error' + } + if (faultCode) { + return 'error' + } + if (onlineStatus) { + var onlineUpper = toUpperText(onlineStatus) + if (onlineUpper === 'OFFLINE') { + return 'offline' + } + if (onlineUpper === 'ERROR' || onlineUpper === 'FAULT') { + return 'error' + } + } + return 'online' +} + +var pickStreamUrl = (row) => { + var hd = pickText(row, 'hdPlayUrl', 'hd_play_url') + if (hd) { + return hd + } + var play = pickText(row, 'playUrl', 'play_url') + if (play) { + return play + } + return pickText(row, 'latestStreamUrl', 'latest_stream_url') +} + +var pickHdStreamUrl = (row) => { + return pickText(row, 'hdPlayUrl', 'hd_play_url') +} + +var pickPlayUrl = (row) => { + return pickText(row, 'playUrl', 'play_url') +} + +var pickPreviewUrl = (row) => { + var preview = pickText(row, 'previewImgUrl', 'preview_img_url') + if (preview) { + return preview + } + var img = pickText(row, 'img', 'img') + if (img) { + return img + } + return pickText(row, 'latestSnapshotUrl', 'latest_snapshot_url') +} + +if (rows && rows.length > 0) { + for (row in rows) { + cameras.push({ + id: pickText(row, 'id', 'id'), + name: pickText(row, 'name', 'name'), + number: pickText(row, 'number', 'number'), + location: pickText(row, 'location', 'location'), + address: pickText(row, 'address', 'address'), + channelNumber: row.channelNumber != null ? row.channelNumber : row.channel_number, + sortIndex: row.sortIndex != null ? row.sortIndex : row.sort_index, + status: mapStatus(row), + resolution: '1920x1080', + preview: pickPreviewUrl(row), + hdStreamUrl: pickHdStreamUrl(row), + playUrl: pickPlayUrl(row), + streamUrl: pickStreamUrl(row) + }) + } +} + +return { + pageSize: 2, + autoPlayInterval: 10000, + cameras: cameras +} diff --git a/data/magic-api/api/大屏数据/市场实时监控播放配置.ms b/data/magic-api/api/大屏数据/市场实时监控播放配置.ms new file mode 100644 index 0000000..171c7be --- /dev/null +++ b/data/magic-api/api/大屏数据/市场实时监控播放配置.ms @@ -0,0 +1,77 @@ +{ + "properties" : { }, + "id" : "d4e5f678901234567890abcdef012345", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "市场实时监控播放配置", + "createTime" : 1781203200000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/market-realtime-player-config", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "市场实时监控播放配置:直播流格式识别规则与播放器参数(HLS/FLV/MP4 等)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 直播流地址格式配置,供前端 hls.js / flv.js 自动选型 + +return { + defaultProtocol: 'hls', + urlPriority: ['hdStreamUrl', 'streamUrl', 'playUrl'], + formatRules: [ + { + type: 'hls', + match: '.m3u8', + library: 'hls.js', + description: 'HLS 直播(萤石 open.ys7.com 等)' + }, + { + type: 'flv', + match: '.flv', + library: 'flv.js', + description: 'HTTP-FLV 直播(萤石 rtmp*open.ys7.com 等)' + }, + { + type: 'mp4', + match: '.mp4', + library: 'native', + description: 'MP4 点播/回放' + }, + { + type: 'webm', + match: '.webm', + library: 'native', + description: 'WebM 点播' + } + ], + playerOptions: { + hls: { + enableWorker: true, + lowLatencyMode: true + }, + flv: { + isLive: true, + hasAudio: true, + hasVideo: true + }, + flvMedia: { + enableWorker: true, + enableStashBuffer: false + } + }, + ys7Templates: { + hdHls: 'https://open.ys7.com/v3/openlive/{deviceSerial}_{channelNo}_1.m3u8', + hls: 'https://open.ys7.com/v3/openlive/{deviceSerial}_{channelNo}_2.m3u8', + hdFlv: 'https://rtmp12open.ys7.com:9188/v3/openlive/{deviceSerial}_{channelNo}_1.flv', + flv: 'https://rtmp12open.ys7.com:9188/v3/openlive/{deviceSerial}_{channelNo}_2.flv' + } +} diff --git a/data/magic-api/api/大屏数据/市场环境监控.ms b/data/magic-api/api/大屏数据/市场环境监控.ms new file mode 100644 index 0000000..0baedcd --- /dev/null +++ b/data/magic-api/api/大屏数据/市场环境监控.ms @@ -0,0 +1,95 @@ +{ + "properties" : { }, + "id" : "b2c3d4e5f678901234567890abcdef01", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "市场环境监控", + "createTime" : 1781196000000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/market-environment", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "市场环境监控:温湿度(iot_device_env_data)+ 气象(iot_device_weather_data)最新一条,只读。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +var envSql = """ +SELECT + temperature, + humidity, + to_char(collect_time, 'YYYY-MM-DD HH24:MI:SS') AS collect_time +FROM iot_device_env_data +ORDER BY collect_time DESC NULLS LAST +LIMIT 1 +""" + +var weatherSql = """ +SELECT + air_pressure, + pm25, + pm10, + uv_index, + rainfall, + wind_speed, + wind_direction, + to_char(collect_time, 'YYYY-MM-DD HH24:MI:SS') AS collect_time +FROM iot_device_weather_data +ORDER BY collect_time DESC NULLS LAST +LIMIT 1 +""" + +var envRows = db.select(envSql) +var weatherRows = db.select(weatherSql) +var env = envRows && envRows.length > 0 ? envRows[0] : null +var weather = weatherRows && weatherRows.length > 0 ? weatherRows[0] : null + +var pickNum = (row, camelKey, snakeKey) => { + if (!row) { + return null + } + if (row[camelKey] != null) { + return row[camelKey] + } + if (row[snakeKey] != null) { + return row[snakeKey] + } + return null +} + +var pickText = (row, camelKey, snakeKey) => { + if (!row) { + return '' + } + if (row[camelKey]) { + return row[camelKey] + } + if (row[snakeKey]) { + return row[snakeKey] + } + return '' +} + +return { + temperature: pickNum(env, 'temperature', 'temperature'), + humidity: pickNum(env, 'humidity', 'humidity'), + envCollectTime: pickText(env, 'collectTime', 'collect_time'), + airPressure: pickNum(weather, 'airPressure', 'air_pressure'), + pm25: pickNum(weather, 'pm25', 'pm25'), + pm10: pickNum(weather, 'pm10', 'pm10'), + uvIndex: pickNum(weather, 'uvIndex', 'uv_index'), + rainfall: pickNum(weather, 'rainfall', 'rainfall'), + windSpeed: pickNum(weather, 'windSpeed', 'wind_speed'), + windDirection: pickNum(weather, 'windDirection', 'wind_direction'), + weatherCollectTime: pickText(weather, 'collectTime', 'collect_time') +} diff --git a/data/magic-api/api/大屏数据/活牛鲜肉价格趋势.ms b/data/magic-api/api/大屏数据/活牛鲜肉价格趋势.ms new file mode 100644 index 0000000..6c5ecb0 --- /dev/null +++ b/data/magic-api/api/大屏数据/活牛鲜肉价格趋势.ms @@ -0,0 +1,116 @@ +{ + "properties" : { }, + "id" : "5e6f7a8490b1c2d3e4f5a678903", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "活牛鲜肉价格趋势", + "createTime" : 1780877200000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/price-trend", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "活牛/鲜肉价格趋势:近8个月市场采集均价。数据源 yak_trade_market_price,活牛=LIVE_YAK,鲜肉=FRESH_MEAT,直接使用 price 字段及 unit 单位,不做重量折算。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 +// 数据源:yak_trade_market_price(市场采集价格,已含 price + unit) +// price_type: LIVE_YAK=活牛, FRESH_MEAT=鲜肉/牛肉 + +var monthBucketSql = """ +SELECT + d.month_start, + d.offs, + TO_CHAR(d.month_start, 'FMMM') || '月' AS label +FROM ( + SELECT + (date_trunc('month', CURRENT_DATE) - (offs || ' months')::interval)::date AS month_start, + offs + FROM generate_series(7, 0, -1) AS offs +) d +ORDER BY d.offs DESC +""" + +var livePriceSql = """ +SELECT + date_trunc('month', p.price_date)::date AS month_start, + ROUND(AVG(p.price)::numeric, 2) AS live_cattle_price +FROM yak_trade_market_price p +WHERE p.price_type = 'LIVE_YAK' + AND p.price_date >= date_trunc('month', CURRENT_DATE) - INTERVAL '7 months' +GROUP BY date_trunc('month', p.price_date)::date +""" + +var beefPriceSql = """ +SELECT + date_trunc('month', p.price_date)::date AS month_start, + ROUND(AVG(p.price)::numeric, 2) AS beef_price +FROM yak_trade_market_price p +WHERE p.price_type = 'FRESH_MEAT' + AND p.price_date >= date_trunc('month', CURRENT_DATE) - INTERVAL '7 months' +GROUP BY date_trunc('month', p.price_date)::date +""" + +var unitSql = """ +SELECT + MAX(CASE WHEN price_type = 'LIVE_YAK' THEN unit END) AS live_unit, + MAX(CASE WHEN price_type = 'FRESH_MEAT' THEN unit END) AS beef_unit +FROM yak_trade_market_price +WHERE price_type IN ('LIVE_YAK', 'FRESH_MEAT') +""" + +var buckets = db.select(monthBucketSql) +var liveRows = db.select(livePriceSql) +var beefRows = db.select(beefPriceSql) +var unitRows = db.select(unitSql) + +var liveMap = {} +var beefMap = {} + +for (row in liveRows) { + liveMap[row.monthStart] = row.liveCattlePrice +} + +for (row in beefRows) { + beefMap[row.monthStart] = row.beefPrice +} + +var liveUnit = '元/公斤' +var beefUnit = '元/公斤' +if (unitRows && unitRows.length > 0) { + if (unitRows[0].liveUnit) { + liveUnit = unitRows[0].liveUnit + } + if (unitRows[0].beefUnit) { + beefUnit = unitRows[0].beefUnit + } +} + +var labels = [] +var liveCattlePrice = [] +var beefPrice = [] + +for (bucket in buckets) { + var monthKey = bucket.monthStart + labels.push(bucket.label) + liveCattlePrice.push(liveMap[monthKey] != null ? liveMap[monthKey] : null) + beefPrice.push(beefMap[monthKey] != null ? beefMap[monthKey] : null) +} + +return { + labels: labels, + liveCattlePrice: liveCattlePrice, + beefPrice: beefPrice, + unit: liveUnit, + liveUnit: liveUnit, + beefUnit: beefUnit +} diff --git a/data/magic-api/api/大屏数据/牦牛供应实时信息.ms b/data/magic-api/api/大屏数据/牦牛供应实时信息.ms new file mode 100644 index 0000000..02d4934 --- /dev/null +++ b/data/magic-api/api/大屏数据/牦牛供应实时信息.ms @@ -0,0 +1,173 @@ +{ + "properties" : { }, + "id" : "a1b2c3d4e5f6478990abcdef12345678", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "牦牛供应实时信息", + "createTime" : 1780882000000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/yak-supply-realtime-info", + "method" : "GET", + "parameters" : [ { + "name" : "pageNo", + "value" : "1", + "description" : "页码,从 1 开始(勿用 page,与 magic-api 内置分页参数冲突)", + "required" : false, + "dataType" : "Integer", + "type" : null, + "defaultValue" : "1", + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + }, { + "name" : "pageSize", + "value" : "5", + "description" : "每页条数", + "required" : false, + "dataType" : "Integer", + "type" : null, + "defaultValue" : "5", + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + } ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "牦牛供应实时信息:卖家进场登记列表与汇总,数据源 yak_trade_entry_record + 耳标库存(只读)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +// page 为 magic-api 内置分页变量,页码参数使用 pageNo +var pageNum = 1 +var sizeNum = 5 +if (pageNo) { + pageNum = pageNo +} +if (pageSize) { + sizeNum = pageSize +} +if (pageNum < 1) { + pageNum = 1 +} +if (sizeNum < 1) { + sizeNum = 5 +} +if (sizeNum > 50) { + sizeNum = 50 +} +var offset = (pageNum - 1) * sizeNum + +var summarySql = """ +SELECT + COUNT(*) FILTER (WHERE entered_at >= CURRENT_DATE) AS today_entries, + COALESCE(SUM(yak_count), 0) AS total_yaks, + COUNT(*) FILTER (WHERE sold_count > 0 AND sold_count < yak_count) AS trading_count, + COUNT(*) AS total_records +FROM ( + SELECT + e.entered_at, + COALESCE(NULLIF(inv.yak_count, 0), et.yak_count, 0) AS yak_count, + COALESCE(inv.sold_count, 0) AS sold_count + FROM yak_trade_entry_record e + LEFT JOIN ( + SELECT + seller_entry_record_id, + COUNT(*) AS yak_count, + COUNT(*) FILTER (WHERE status = 'SOLD') AS sold_count + FROM yak_trade_ear_tag_inventory + GROUP BY seller_entry_record_id + ) inv ON inv.seller_entry_record_id = e.id + LEFT JOIN ( + SELECT entry_record_id, COUNT(*) AS yak_count + FROM yak_trade_entry_record_ear_tag + GROUP BY entry_record_id + ) et ON et.entry_record_id = e.id + WHERE e.entry_type = 'SELLER' +) t +""" + +var listSql = """ +SELECT + e.id, + e.name, + e.vehicle_no, + e.phone, + COALESCE(c.region_name, c.address, '') AS origin, + COALESCE(e.quarantine_certificate_no, '') AS quarantine_no, + to_char(e.entered_at, 'YYYY-MM-DD HH24:MI') AS entry_time, + COALESCE(NULLIF(inv.yak_count, 0), et.yak_count, 0) AS yak_count, + COALESCE(inv.sold_count, 0) AS sold_count, + CASE + WHEN COALESCE(NULLIF(inv.yak_count, 0), et.yak_count, 0) = 0 THEN 0 + ELSE ROUND( + COALESCE(inv.sold_count, 0) * 100.0 + / COALESCE(NULLIF(inv.yak_count, 0), et.yak_count, 0) + )::int + END AS progress +FROM yak_trade_entry_record e +LEFT JOIN ( + SELECT + seller_entry_record_id, + COUNT(*) AS yak_count, + COUNT(*) FILTER (WHERE status = 'SOLD') AS sold_count + FROM yak_trade_ear_tag_inventory + GROUP BY seller_entry_record_id +) inv ON inv.seller_entry_record_id = e.id +LEFT JOIN ( + SELECT entry_record_id, COUNT(*) AS yak_count + FROM yak_trade_entry_record_ear_tag + GROUP BY entry_record_id +) et ON et.entry_record_id = e.id +LEFT JOIN yak_sn_customer c + ON c.id = e.party_id + AND c.del_flag = '0' +WHERE e.entry_type = 'SELLER' +ORDER BY e.entered_at DESC +LIMIT #{sizeNum} OFFSET #{offset} +""" + +var summaryRows = db.select(summarySql) +var summaryRow = summaryRows && summaryRows.length > 0 ? summaryRows[0] : null +var rows = db.select(listSql) +var list = [] + +for (row in rows) { + var yakCount = row.yakCount ? row.yakCount : (row.yak_count ? row.yak_count : 0) + var soldCount = row.soldCount ? row.soldCount : (row.sold_count ? row.sold_count : 0) + var progress = row.progress ? row.progress : 0 + + list.push({ + id: row.id, + name: row.name ? row.name : '', + licensePlate: row.vehicleNo ? row.vehicleNo : (row.vehicle_no ? row.vehicle_no : ''), + yakCount: yakCount, + contact: row.phone ? row.phone : '', + origin: row.origin ? row.origin : '', + quarantineNo: row.quarantineNo ? row.quarantineNo : (row.quarantine_no ? row.quarantine_no : ''), + entryTime: row.entryTime ? row.entryTime : (row.entry_time ? row.entry_time : ''), + progress: progress, + tradedCount: soldCount, + pendingCount: yakCount - soldCount + }) +} + +return { + summary: { + todayEntries: summaryRow && summaryRow.todayEntries ? summaryRow.todayEntries : (summaryRow && summaryRow.today_entries ? summaryRow.today_entries : 0), + totalYaks: summaryRow && summaryRow.totalYaks ? summaryRow.totalYaks : (summaryRow && summaryRow.total_yaks ? summaryRow.total_yaks : 0), + tradingCount: summaryRow && summaryRow.tradingCount ? summaryRow.tradingCount : (summaryRow && summaryRow.trading_count ? summaryRow.trading_count : 0) + }, + total: summaryRow && summaryRow.totalRecords ? summaryRow.totalRecords : (summaryRow && summaryRow.total_records ? summaryRow.total_records : 0), + list: list +} diff --git a/data/magic-api/api/大屏数据/牦牛供应详情.ms b/data/magic-api/api/大屏数据/牦牛供应详情.ms new file mode 100644 index 0000000..b005d43 --- /dev/null +++ b/data/magic-api/api/大屏数据/牦牛供应详情.ms @@ -0,0 +1,344 @@ +{ + "properties" : { }, + "id" : "b2c3d4e5f6a7488990abcdef123456789", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "牦牛供应详情", + "createTime" : 1780882100000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/yak-supply-detail", + "method" : "GET", + "parameters" : [ { + "name" : "id", + "value" : null, + "description" : "进场登记 ID(yak_trade_entry_record.id)", + "required" : true, + "dataType" : "String", + "type" : null, + "defaultValue" : null, + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + } ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "牦牛供应单条详情:yak_trade_entry_record + 耳标库存 + 订单/称重 + 证件图(只读)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +if (!id) { + exit 400, '缺少参数 id' +} + +var pickUrl = (value) => { + if (!value) { + return '' + } + var text = (value + '').trim() + if (!text) { + return '' + } + if (text.indexOf('data:') === 0) { + return text + } + if (text.indexOf('http://localhost:8080') === 0) { + return text.substring('http://localhost:8080'.length) + } + if (text.indexOf('http://127.0.0.1:8080') === 0) { + return text.substring('http://127.0.0.1:8080'.length) + } + if (text.indexOf('http') === 0 || text.indexOf('/') === 0) { + return text + } + if (text.indexOf('profile/') === 0) { + return '/' + text + } + if (text.match(/^\d{4}\/\d{2}\/\d{2}\//)) { + return '/profile/' + text + } + return '' +} + +var baseSql = """ +SELECT + e.id, + e.name, + e.vehicle_no, + e.phone, + e.party_id, + COALESCE(c.region_name, c.address, '') AS origin, + COALESCE(e.quarantine_certificate_no, '') AS quarantine_no, + to_char(e.entered_at, 'YYYY-MM-DD HH24:MI') AS entry_time, + COALESCE(NULLIF(inv.yak_count, 0), et.yak_count, 0) AS yak_count, + COALESCE(inv.sold_count, 0) AS sold_count, + e.entry_photo_id, + cert.certificate_image_file_id, + cr.entry_image AS car_entry_image, + entry_oss.url AS entry_oss_url, + cert_oss.url AS cert_oss_url, + COALESCE(ord.order_count, 0) AS order_count +FROM yak_trade_entry_record e +LEFT JOIN ( + SELECT + seller_entry_record_id, + COUNT(*) AS yak_count, + COUNT(*) FILTER (WHERE status = 'SOLD') AS sold_count + FROM yak_trade_ear_tag_inventory + GROUP BY seller_entry_record_id +) inv ON inv.seller_entry_record_id = e.id +LEFT JOIN ( + SELECT entry_record_id, COUNT(*) AS yak_count + FROM yak_trade_entry_record_ear_tag + GROUP BY entry_record_id +) et ON et.entry_record_id = e.id +LEFT JOIN yak_sn_customer c + ON c.id = e.party_id + AND c.del_flag = '0' +LEFT JOIN LATERAL ( + SELECT certificate_image_file_id + FROM yak_trade_entry_record_ear_tag t + WHERE t.entry_record_id = e.id + AND t.certificate_image_file_id IS NOT NULL + AND TRIM(t.certificate_image_file_id) <> '' + ORDER BY t.create_time + LIMIT 1 +) cert ON TRUE +LEFT JOIN LATERAL ( + SELECT entry_image + FROM yak_car_record cr + WHERE cr.del_flag = '0' + AND cr.plate_no = e.vehicle_no + ORDER BY cr.entry_time DESC NULLS LAST + LIMIT 1 +) cr ON TRUE +LEFT JOIN yak_trade_quarantine_certificate qc + ON qc.certificate_no = e.quarantine_certificate_no +LEFT JOIN LATERAL ( + SELECT o.url + FROM sys_oss o + WHERE e.entry_photo_id IS NOT NULL + AND TRIM(e.entry_photo_id) <> '' + AND ( + o.oss_id::text = e.entry_photo_id + OR o.ext1 = e.entry_photo_id + OR o.file_name ILIKE '%' || e.entry_photo_id || '%' + OR o.original_name ILIKE '%' || e.entry_photo_id || '%' + OR o.url ILIKE '%' || e.entry_photo_id || '%' + ) + ORDER BY + CASE + WHEN o.oss_id::text = e.entry_photo_id THEN 0 + WHEN o.ext1 = e.entry_photo_id THEN 1 + ELSE 2 + END + LIMIT 1 +) entry_oss ON TRUE +LEFT JOIN LATERAL ( + SELECT o.url + FROM sys_oss o + WHERE ( + cert.certificate_image_file_id IS NOT NULL + AND TRIM(cert.certificate_image_file_id) <> '' + AND ( + o.oss_id::text = cert.certificate_image_file_id + OR o.ext1 = cert.certificate_image_file_id + OR o.file_name ILIKE '%' || cert.certificate_image_file_id || '%' + OR o.original_name ILIKE '%' || cert.certificate_image_file_id || '%' + OR o.url ILIKE '%' || cert.certificate_image_file_id || '%' + ) + ) + OR ( + qc.image_file_id IS NOT NULL + AND TRIM(qc.image_file_id) <> '' + AND ( + o.oss_id::text = qc.image_file_id + OR o.ext1 = qc.image_file_id + OR o.file_name ILIKE '%' || qc.image_file_id || '%' + OR o.original_name ILIKE '%' || qc.image_file_id || '%' + OR o.url ILIKE '%' || qc.image_file_id || '%' + ) + ) + ORDER BY + CASE + WHEN cert.certificate_image_file_id IS NOT NULL + AND o.oss_id::text = cert.certificate_image_file_id THEN 0 + WHEN qc.image_file_id IS NOT NULL + AND o.oss_id::text = qc.image_file_id THEN 1 + ELSE 2 + END + LIMIT 1 +) cert_oss ON TRUE +LEFT JOIN ( + SELECT + inv.seller_entry_record_id, + COUNT(DISTINCT o.id) AS order_count + FROM yak_trade_ear_tag_inventory inv + JOIN yak_trade_order_item i ON i.ear_tag_no = inv.ear_tag_no + JOIN yak_trade_order o ON o.id = i.order_id + WHERE o.status <> 'CANCELLED' + GROUP BY inv.seller_entry_record_id +) ord ON ord.seller_entry_record_id = e.id +WHERE e.id = #{id} + AND e.entry_type = 'SELLER' +""" + +var orderSql = """ +SELECT + o.id, + COALESCE(bc.customer_name, be.name, o.buyer_id, '') AS buyer, + COALESCE(sc.customer_name, e.name, o.seller_id, '') AS seller, + to_char(o.created_at, 'YYYY-MM-DD HH24:MI') AS trade_time, + COALESCE(SUM(COALESCE(i.quantity, 1)), 0) AS quantity, + COALESCE(SUM(i.weight), 0) AS weight, + MAX(w.photo_id) AS weight_photo_id, + MAX(wo.url) AS weight_photo_url +FROM yak_trade_entry_record e +JOIN yak_trade_ear_tag_inventory inv + ON inv.seller_entry_record_id = e.id +JOIN yak_trade_order_item i + ON i.ear_tag_no = inv.ear_tag_no +JOIN yak_trade_order o + ON o.id = i.order_id +LEFT JOIN yak_sn_customer bc + ON bc.id = o.buyer_id + AND bc.del_flag = '0' +LEFT JOIN yak_sn_customer sc + ON sc.id = o.seller_id + AND sc.del_flag = '0' +LEFT JOIN yak_trade_entry_record be + ON be.party_id = o.buyer_id + AND be.entry_type = 'BUYER' +LEFT JOIN yak_trade_weighing_record w + ON w.id = i.weighing_record_id +LEFT JOIN LATERAL ( + SELECT o.url + FROM sys_oss o + WHERE w.photo_id IS NOT NULL + AND TRIM(w.photo_id) <> '' + AND ( + o.oss_id::text = w.photo_id + OR o.ext1 = w.photo_id + OR o.file_name ILIKE '%' || w.photo_id || '%' + OR o.original_name ILIKE '%' || w.photo_id || '%' + OR o.url ILIKE '%' || w.photo_id || '%' + ) + ORDER BY + CASE + WHEN o.oss_id::text = w.photo_id THEN 0 + WHEN o.ext1 = w.photo_id THEN 1 + ELSE 2 + END + LIMIT 1 +) wo ON TRUE +WHERE e.id = #{id} + AND o.status <> 'CANCELLED' +GROUP BY o.id, bc.customer_name, be.name, sc.customer_name, e.name, o.buyer_id, o.seller_id, o.created_at +ORDER BY o.created_at DESC +""" + +var resolveOssUrl = (ossUrl, rawId) => { + var url = pickUrl(ossUrl) + if (url) { + return url + } + if (!rawId) { + return '' + } + var idText = (rawId + '').trim().replace(/'/g, "''") + if (!idText) { + return '' + } + var lookupSql = """ + SELECT url FROM sys_oss + WHERE oss_id::text = '""" + idText + """' + OR ext1 = '""" + idText + """' + OR file_name ILIKE '%""" + idText + """%' + OR original_name ILIKE '%""" + idText + """%' + OR url ILIKE '%""" + idText + """%' + ORDER BY + CASE + WHEN oss_id::text = '""" + idText + """' THEN 0 + WHEN ext1 = '""" + idText + """' THEN 1 + ELSE 2 + END + LIMIT 1 + """ + var rows = db.select(lookupSql) + if (rows && rows.length > 0) { + return pickUrl(rows[0].url) + } + return pickUrl(idText) +} + +var baseRows = db.select(baseSql) +if (!baseRows || baseRows.length == 0) { + exit 404, '未找到供应记录' +} + +var base = baseRows[0] +var yakCount = base.yakCount ? base.yakCount : (base.yak_count ? base.yak_count : 0) +var soldCount = base.soldCount ? base.soldCount : (base.sold_count ? base.sold_count : 0) +var orderCount = base.orderCount ? base.orderCount : (base.order_count ? base.order_count : 0) +var progress = 0 +if (yakCount > 0) { + progress = Math.round(soldCount * 100 / yakCount) +} + +var entryPhotoId = base.entryPhotoId ? base.entryPhotoId : (base.entry_photo_id ? base.entry_photo_id : '') +var certFileId = base.certificateImageFileId ? base.certificateImageFileId : (base.certificate_image_file_id ? base.certificate_image_file_id : '') + +var entryPhoto = resolveOssUrl(base.entryOssUrl ? base.entryOssUrl : base.entry_oss_url, entryPhotoId) +if (!entryPhoto) { + entryPhoto = pickUrl(base.carEntryImage ? base.carEntryImage : base.car_entry_image) +} + +var quarantineCert = resolveOssUrl(base.certOssUrl ? base.certOssUrl : base.cert_oss_url, certFileId) + +var orderRows = db.select(orderSql) +var orders = [] + +for (order in orderRows) { + var weightPhotoId = order.weightPhotoId ? order.weightPhotoId : (order.weight_photo_id ? order.weight_photo_id : '') + var weightPhoto = resolveOssUrl(order.weightPhotoUrl ? order.weightPhotoUrl : order.weight_photo_url, weightPhotoId) + + orders.push({ + id: order.id, + buyer: order.buyer ? order.buyer : '', + seller: order.seller ? order.seller : '', + tradeTime: order.tradeTime ? order.tradeTime : (order.trade_time ? order.trade_time : ''), + quantity: order.quantity ? order.quantity : 0, + weight: order.weight ? order.weight : 0, + weightPhoto: weightPhoto, + weightPhotoId: weightPhotoId + }) +} + +return { + id: base.id, + name: base.name ? base.name : '', + licensePlate: base.vehicleNo ? base.vehicleNo : (base.vehicle_no ? base.vehicle_no : ''), + yakCount: yakCount, + contact: base.phone ? base.phone : '', + origin: base.origin ? base.origin : '', + quarantineNo: base.quarantineNo ? base.quarantineNo : (base.quarantine_no ? base.quarantine_no : ''), + entryTime: base.entryTime ? base.entryTime : (base.entry_time ? base.entry_time : ''), + progress: progress, + tradedCount: soldCount, + pendingCount: yakCount - soldCount, + orderCount: orderCount, + entryPhoto: entryPhoto, + entryPhotoId: entryPhotoId, + quarantineCert: quarantineCert, + quarantineCertId: certFileId, + orders: orders +} diff --git a/data/magic-api/api/大屏数据/牦牛销售类型统计.ms b/data/magic-api/api/大屏数据/牦牛销售类型统计.ms new file mode 100644 index 0000000..cc43759 --- /dev/null +++ b/data/magic-api/api/大屏数据/牦牛销售类型统计.ms @@ -0,0 +1,73 @@ +{ + "properties" : { }, + "id" : "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "牦牛销售类型统计", + "createTime" : 1780880400000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/yak-sales-type-stats", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "牦牛销售类型统计:按订单 purpose 归类为屠宰/养殖/其他,统计已完成订单牦牛头数与占比(只读)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 +// 数据源 yak_sn_order.purpose,按用途关键词归类 + +var sql = """ +SELECT category AS name, COALESCE(SUM(quantity), 0) AS value +FROM ( + SELECT + o.quantity, + CASE + WHEN TRIM(COALESCE(o.purpose, '')) ~* '(屠宰|餐饮|肉食|宰杀)' THEN '屠宰用途' + WHEN TRIM(COALESCE(o.purpose, '')) ~* '(养殖|繁育|种|批发|转售)' THEN '养殖用途' + ELSE '其他用途' + END AS category + FROM yak_sn_order o + WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' +) t +GROUP BY category +""" + +var rows = db.select(sql) +var categories = ['屠宰用途', '养殖用途', '其他用途'] +var valueMap = {} + +for (row in rows) { + valueMap[row.name] = row.value ? row.value : 0 +} + +var total = 0 +for (cat in categories) { + var count = valueMap[cat] ? valueMap[cat] : 0 + total = total + count +} + +var result = [] +for (cat in categories) { + var count = valueMap[cat] ? valueMap[cat] : 0 + var percent = 0 + if (total > 0) { + percent = (count * 100.0) / total + } + result.push({ + name: cat, + value: count, + percent: percent + }) +} + +return result diff --git a/data/magic-api/api/大屏数据/系统配置.ms b/data/magic-api/api/大屏数据/系统配置.ms new file mode 100644 index 0000000..045195e --- /dev/null +++ b/data/magic-api/api/大屏数据/系统配置.ms @@ -0,0 +1,74 @@ +{ + "properties" : { }, + "id" : "a3c4d5e6f7a8490b1c2d3e4f5a67890", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "系统配置", + "createTime" : 1780876900000, + "updateTime" : 1780882000000, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/system-config", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "大屏系统配置:标题、标题背景、地图枢纽节点(红原)及坐标映射等", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 默认配置(可在 magic-api 编辑器中直接修改) +// mapHub.name 需与「地图迁徙数据」接口中的 hubName 保持一致 + +var config = { + title: '/images/标题.png', + titleBackground: '/images/标题背景.png', + mapHub: { + name: '红原县', + coordinates: [102.568685, 32.826358], + description: '四川省阿坝州红原县,中国重要的牦牛养殖基地', + localOriginKeyword: '红原' + }, + mapFlowLevels: { + high: { threshold: 80, color: '#E6A23C', description: '主要流向' }, + medium: { threshold: 40, color: '#409EFF', description: '重要流向' }, + low: { threshold: 0, color: '#67C23A', description: '一般流向' } + }, + mapGeoCoordMap: { + '红原县': [102.568685, 32.826358], + '邛溪镇': [102.515, 32.7855], + '龙日镇': [102.484, 32.3605], + '麦洼乡': [102.918, 32.9156], + '阿木乡': [102.789, 32.9154], + '刷经寺镇': [102.661, 31.9177], + '瓦切镇': [102.669, 33.2578], + '查尔玛乡': [101.921, 32.5029], + '江茸乡': [102.412, 32.3022], + '安曲镇': [102.222, 32.6709], + '色地镇': [102.999, 32.865], + '成都': [103.958004, 30.772708], + '重庆': [106.54, 29.59], + '北京': [116.46, 39.92], + '上海': [121.48, 31.22], + '天津': [117.20, 39.13], + '拉萨': [91.420246, 29.878931], + '西宁': [101.74113, 36.641678], + '兰州': [103.249708, 35.956623], + '西安': [108.95, 34.27], + '昆明': [102.72, 25.05], + '贵阳': [106.71, 26.57], + '康定': [101.956, 30.057], + '香格里拉': [99.708, 27.825], + '合作': [102.911, 34.986], + '甘南藏族自治州': [103.3049, 34.644479], + '玉树': [97.008, 33.004], + '甘孜藏族自治州': [101.226006, 30.470387] + } +} + +return config diff --git a/data/magic-api/api/大屏数据/综合销售统计.ms b/data/magic-api/api/大屏数据/综合销售统计.ms new file mode 100644 index 0000000..98a8b96 --- /dev/null +++ b/data/magic-api/api/大屏数据/综合销售统计.ms @@ -0,0 +1,157 @@ +{ + "properties" : { }, + "id" : "4d5e6f7a8490b1c2d3e4f5a678902", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "综合销售统计", + "createTime" : 1780877100000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/comprehensive-sales-stats", + "method" : "GET", + "parameters" : [ { + "name" : "type", + "value" : null, + "description" : "统计类型:monthlySales(本月销售)/overallSalesDistribution(总体销售)/purchaseRegionDistribution(采购地区),不传则返回全部", + "required" : false, + "dataType" : "String", + "type" : null, + "defaultValue" : null, + "validateType" : null, + "error" : null, + "expression" : null, + "children" : null + } ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "综合销售统计:本月销售/总体销售/采购地区三维饼图数据,数据源 yak_sn_order 已完成订单(只读)", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 数据源 yak_sn_order + yak_sn_customer +// value = 牦牛交易头数 quantity + +var mapRows = (rows) => { + var total = 0 + var result = [] + + for (row in rows) { + total = total + (row.value ? row.value : 0) + } + + for (row in rows) { + var value = row.value ? row.value : 0 + var percent = 0 + if (total > 0) { + percent = (value * 100) / total + } + result.push({ + name: row.name, + value: value, + percent: percent + }) + } + + return result +} + +var monthlySql = """ +SELECT town AS name, COALESCE(SUM(quantity), 0) AS value +FROM ( + SELECT + o.quantity, + COALESCE( + NULLIF((regexp_match(o.origin_place, '([^省市区县]+(?:镇|乡|街道))'))[1], ''), + NULLIF(TRIM(o.origin_place), ''), + '未知地区' + ) AS town + FROM yak_sn_order o + CROSS JOIN ( + SELECT CASE + WHEN EXISTS ( + SELECT 1 FROM yak_sn_order x + WHERE x.del_flag = '0' + AND x.status = 'COMPLETED' + AND x.transaction_time >= date_trunc('month', CURRENT_DATE) + AND x.transaction_time < date_trunc('month', CURRENT_DATE) + INTERVAL '1 month' + ) THEN date_trunc('month', CURRENT_DATE) + ELSE date_trunc('month', ( + SELECT MAX(transaction_time) FROM yak_sn_order + WHERE del_flag = '0' AND status = 'COMPLETED' + )) + END AS month_start + ) m + WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' + AND o.transaction_time >= m.month_start + AND o.transaction_time < m.month_start + INTERVAL '1 month' +) t +GROUP BY town +ORDER BY value DESC +LIMIT 5 +""" + +var overallSql = """ +SELECT town AS name, COALESCE(SUM(quantity), 0) AS value +FROM ( + SELECT + o.quantity, + COALESCE( + NULLIF((regexp_match(o.origin_place, '([^省市区县]+(?:镇|乡|街道))'))[1], ''), + NULLIF(TRIM(o.origin_place), ''), + '未知地区' + ) AS town + FROM yak_sn_order o + WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' +) t +GROUP BY town +ORDER BY value DESC +LIMIT 5 +""" + +var purchaseSql = """ +SELECT region AS name, COALESCE(SUM(quantity), 0) AS value +FROM ( + SELECT + o.quantity, + COALESCE( + NULLIF(TRIM(b.region_name), ''), + NULLIF((regexp_match(o.destination_place, '([^省市区县]+(?:市|州|盟|县|区))'))[1], ''), + NULLIF(TRIM(o.destination_place), ''), + '未知地区' + ) AS region + FROM yak_sn_order o + LEFT JOIN yak_sn_customer b + ON b.id = o.buyer_id + AND b.del_flag = '0' + WHERE o.del_flag = '0' + AND o.status = 'COMPLETED' +) t +GROUP BY region +ORDER BY value DESC +LIMIT 5 +""" + +var monthlyRows = db.select(monthlySql) +var overallRows = db.select(overallSql) +var purchaseRows = db.select(purchaseSql) + +var result = { + monthlySales: mapRows(monthlyRows), + overallSalesDistribution: mapRows(overallRows), + purchaseRegionDistribution: mapRows(purchaseRows) +} + +var t = type +if (t && result[t]) { + return result[t] +} + +return result diff --git a/data/magic-api/api/大屏数据/采购商户来源分析.ms b/data/magic-api/api/大屏数据/采购商户来源分析.ms new file mode 100644 index 0000000..228819a --- /dev/null +++ b/data/magic-api/api/大屏数据/采购商户来源分析.ms @@ -0,0 +1,55 @@ +{ + "properties" : { }, + "id" : "8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3", + "script" : null, + "groupId" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", + "name" : "采购商户来源分析", + "createTime" : 1780881000000, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : "admin", + "path" : "/buyer-source-analysis", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : "", + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : "采购商户来源分析:按地区统计采购商户(BUYER)数量 Top9,数据源 yak_sn_customer(只读)。", + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +// 只读查询,不修改任何数据 + +var sql = """ +SELECT + region_name AS name, + region_name AS full_name, + COUNT(*) AS value, + region_name || '采购商户' AS description +FROM yak_sn_customer c +WHERE c.del_flag = '0' + AND c.customer_type = 'BUYER' + AND c.region_name IS NOT NULL + AND TRIM(c.region_name) <> '' +GROUP BY region_name +ORDER BY value DESC, region_name +LIMIT 9 +""" + +var rows = db.select(sql) +var result = [] + +for (row in rows) { + result.push({ + name: row.name, + value: row.value ? row.value : 0, + fullName: row.fullName ? row.fullName : (row.full_name ? row.full_name : ''), + description: row.description ? row.description : '采购商户' + }) +} + +return result diff --git a/data/magic-api/api/测试分组/group.json b/data/magic-api/api/测试分组/group.json new file mode 100644 index 0000000..1e8c734 --- /dev/null +++ b/data/magic-api/api/测试分组/group.json @@ -0,0 +1,14 @@ +{ + "properties" : { }, + "id" : "d9addaf6c9904669b19434d023e8e55e", + "name" : "测试分组", + "type" : "api", + "parentId" : "0", + "path" : "/test", + "createTime" : 1780801394664, + "updateTime" : null, + "createBy" : "admin", + "updateBy" : null, + "paths" : [ ], + "options" : [ ] +} \ No newline at end of file diff --git a/data/magic-api/api/测试分组/测试接口.ms b/data/magic-api/api/测试分组/测试接口.ms new file mode 100644 index 0000000..9283fce --- /dev/null +++ b/data/magic-api/api/测试分组/测试接口.ms @@ -0,0 +1,25 @@ +{ + "properties" : { }, + "id" : "df98822a60294a7ea789d1f81f1e0e85", + "script" : null, + "groupId" : "d9addaf6c9904669b19434d023e8e55e", + "name" : "测试接口", + "createTime" : 1780801413284, + "updateTime" : null, + "lock" : null, + "createBy" : "admin", + "updateBy" : null, + "path" : "/api1", + "method" : "GET", + "parameters" : [ ], + "options" : [ ], + "requestBody" : null, + "headers" : [ ], + "paths" : [ ], + "responseBody" : null, + "description" : null, + "requestBodyDefinition" : null, + "responseBodyDefinition" : null +} +================================ +return 'Hello magic-api' \ No newline at end of file diff --git a/magic-api-dashboard-interfaces.txt b/magic-api-dashboard-interfaces.txt new file mode 100644 index 0000000..53cee84 --- /dev/null +++ b/magic-api-dashboard-interfaces.txt @@ -0,0 +1,234 @@ +大屏 Magic-API 接口速查 +======================================== +分组:/dashboard +前端代理:/api/dashboard/* +源文件:livestock-trading-backend/data/magic-api/api/大屏数据/*.ms +说明:均为只读;复杂逻辑见各 .ms 文件。修改 .ms 后需重启后端。 + + +-------------------------------------------------------------------------------- +1. 系统配置 +-------------------------------------------------------------------------------- +GET /dashboard/system-config +文件:系统配置.ms | 前端:Dashboard.vue、ChinaMap.vue、systemConfig.js + +业务:大屏静态配置(标题图、地图枢纽红原、城市坐标、流向等级配色等)。 + +数据表:无(脚本内硬编码,可在 magic-api 或 .ms 中改) + + +-------------------------------------------------------------------------------- +2. 实时交易统计 +-------------------------------------------------------------------------------- +GET /dashboard/real-time-stats +文件:实时交易统计.ms | 前端:RealTimeStats.vue + +业务:按时间维度统计交易所已完成订单的交易量、订单数、买卖商户数。 + +参数:dimension(可选)day / week / month / year,不传返回全部 + +表:yak_sn_order +字段:quantity, seller_id, buyer_id, transaction_time, status, del_flag +条件:del_flag='0',status='COMPLETED' + + +-------------------------------------------------------------------------------- +3. 交易所牦牛成交数据 +-------------------------------------------------------------------------------- +GET /dashboard/yak-trading-data +文件:交易所牦牛成交数据.ms | 前端:YakTradingData.vue + +业务:近 6 个日/周/月时间桶的牦牛头数与订单数趋势。 + +参数:period(可选)day / week / month,不传返回全部 + +表:yak_sn_order +字段:id, quantity, transaction_time, status, del_flag +条件:del_flag='0',status='COMPLETED' + + +-------------------------------------------------------------------------------- +4. 综合销售统计 +-------------------------------------------------------------------------------- +GET /dashboard/comprehensive-sales-stats +文件:综合销售统计.ms | 前端:ComprehensiveSalesStats.vue + +业务:三个饼图——本月销售、总体销售(按产地乡镇 Top5)、采购地区(Top5)。 + +参数:type(可选)monthlySales / overallSalesDistribution / purchaseRegionDistribution + +表:yak_sn_order;采购地区 LEFT JOIN yak_sn_customer +字段: + yak_sn_order — quantity, origin_place, destination_place, buyer_id, transaction_time, status, del_flag + yak_sn_customer — id, region_name, del_flag +条件:del_flag='0',status='COMPLETED' + + +-------------------------------------------------------------------------------- +5. 活牛/鲜肉价格趋势 +-------------------------------------------------------------------------------- +GET /dashboard/price-trend +文件:活牛鲜肉价格趋势.ms | 前端:YakPriceTrend.vue + +业务:近 8 个月活牛、鲜肉市场采集均价(不做重量折算)。 + +表:yak_trade_market_price +字段:price_date, price_type(LIVE_YAK / FRESH_MEAT), price, unit + + +-------------------------------------------------------------------------------- +6. 交易中心实时服务信息 +-------------------------------------------------------------------------------- +GET /dashboard/exchange-service-info +文件:交易中心实时服务信息.ms | 前端:ExchangeMonitor.vue + +业务:交易中心实时概览——供应/待售/已售头数、剩余车位、今日进场车辆、供应商数。 + +表及字段: + yak_trade_ear_tag_inventory — status(计总供应/待售/已售) + yak_car_parking_zone — total_capacity, current_count, status + yak_car_record — plate_no, entry_time, del_flag + yak_trade_entry_record — vehicle_no, entered_at + yak_sn_customer — customer_type='SELLER', del_flag + + +-------------------------------------------------------------------------------- +7. 牦牛销售类型统计 +-------------------------------------------------------------------------------- +GET /dashboard/yak-sales-type-stats +文件:牦牛销售类型统计.ms | 前端:YakSalesTypeStats.vue + +业务:按订单用途归类为屠宰/养殖/其他,统计头数与占比。 + +表:yak_sn_order +字段:quantity, purpose, status, del_flag +条件:del_flag='0',status='COMPLETED' + + +-------------------------------------------------------------------------------- +8. 采购商户来源分析 +-------------------------------------------------------------------------------- +GET /dashboard/buyer-source-analysis +文件:采购商户来源分析.ms | 前端:PurchaserAnalysis.vue + +业务:按地区统计采购商户(BUYER)数量 Top9。 + +表:yak_sn_customer +字段:region_name, customer_type, del_flag +条件:customer_type='BUYER',del_flag='0',region_name 非空 + + +-------------------------------------------------------------------------------- +9. 牦牛供应实时信息 +-------------------------------------------------------------------------------- +GET /dashboard/yak-supply-realtime-info +文件:牦牛供应实时信息.ms | 前端:SupplyDemandData.vue + +业务:卖方进场登记列表(分页)及汇总(今日进场、总头数、交易中批次)。 + +参数:pageNo(默认 1)、pageSize(默认 5;勿用 page) + +表及字段: + yak_trade_entry_record — id, name, vehicle_no, phone, party_id, quarantine_certificate_no, entered_at, entry_type + yak_trade_ear_tag_inventory — seller_entry_record_id, status, ear_tag_no + yak_trade_entry_record_ear_tag — entry_record_id + yak_sn_customer — id, region_name, address +条件:entry_type='SELLER' + + +-------------------------------------------------------------------------------- +10. 牦牛供应详情 +-------------------------------------------------------------------------------- +GET /dashboard/yak-supply-detail +文件:牦牛供应详情.ms | 前端:SupplyDemandData.vue + +业务:单条卖方进场详情(基本信息、耳标进度、关联成交订单、进场/检疫/称重图片 URL)。 + +参数:id(必填,yak_trade_entry_record.id) + +表及字段: + yak_trade_entry_record — id, name, vehicle_no, phone, party_id, quarantine_certificate_no, entry_photo_id, entered_at, entry_type + yak_trade_ear_tag_inventory — seller_entry_record_id, ear_tag_no, status, locked_order_id + yak_trade_entry_record_ear_tag — entry_record_id, certificate_image_file_id + yak_trade_order / yak_trade_order_item — 订单与耳标明细 + yak_trade_weighing_record — weight, photo_id + yak_trade_quarantine_certificate — certificate_no, image_file_id + yak_sn_customer — region_name, address(买卖方名称) + yak_car_record — 关联车辆 + sys_oss — url(图片解析) + + +-------------------------------------------------------------------------------- +11. 市场环境监控 +-------------------------------------------------------------------------------- +GET /dashboard/market-environment +文件:市场环境监控.ms | 前端:MarketEnvironmentMonitor.vue + +业务:环境温湿度 + 气象数据各取最新一条。 + +表及字段: + iot_device_env_data — temperature, humidity, collect_time + iot_device_weather_data — air_pressure, pm25, pm10, uv_index, rainfall, wind_speed, wind_direction, collect_time + + +-------------------------------------------------------------------------------- +12. 市场实时监控 +-------------------------------------------------------------------------------- +GET /dashboard/market-realtime-monitor +文件:市场实时监控.ms | 前端:MarketRealtimeMonitor.vue + +业务:视频监控设备列表及播放/预览地址。 + +表及字段: + iot_device_video — id, name, number, location, address, status, play_url, hd_play_url, preview_img_url, channel_number, index, del_flag, is_show + iot_device_video_data — device_id, stream_url, snapshot_url, online_status, stream_status, fault_code, collect_time +条件:del_flag='0',is_show=true + + +-------------------------------------------------------------------------------- +13. 市场实时监控播放配置 +-------------------------------------------------------------------------------- +GET /dashboard/market-realtime-player-config +文件:市场实时监控播放配置.ms | 前端:MarketRealtimeMonitor.vue(liveStreamPlayer.js) + +业务:直播流协议识别规则与 hls.js / flv.js 播放器参数(静态配置)。 + +数据表:无 + + +-------------------------------------------------------------------------------- +14. 地图迁徙数据 +-------------------------------------------------------------------------------- +GET /dashboard/map-trading-network +文件:地图迁徙数据.ms | 前端:ChinaMap.vue + +业务:中央地图三种模式流向——销售网络(红原→全国)、源地供应(全国→红原)、红原出栏(乡镇→红原)。 + +表:yak_sn_order;outflow/inflow LEFT JOIN yak_sn_customer +字段: + yak_sn_order — quantity, origin_place, destination_place, buyer_id, seller_id, status, del_flag + yak_sn_customer — id, region_name, del_flag +条件:del_flag='0',status='COMPLETED';local 模式另要求 origin_place 含「红原」 +关联:系统配置 mapHub.name 与脚本 hubName 需一致(默认「红原县」) + + +================================================================================ +表 → 接口索引 +================================================================================ +yak_sn_order 2, 3, 4, 7, 14 +yak_sn_customer 4, 6, 8, 9, 10, 14 +yak_trade_market_price 5 +yak_trade_ear_tag_inventory 6, 9, 10 +yak_trade_entry_record 6, 9, 10 +yak_trade_entry_record_ear_tag 9, 10 +yak_trade_order / _item 10 +yak_trade_weighing_record 10 +yak_trade_quarantine_certificate 10 +yak_car_parking_zone 6 +yak_car_record 6, 10 +sys_oss 10 +iot_device_env_data 11 +iot_device_weather_data 11 +iot_device_video 12 +iot_device_video_data 12 +(无表) 1, 13 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ae24cac --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.livestock + livestock-trading-backend + 1.0.0-SNAPSHOT + livestock-trading-backend + Livestock trading backend based on magic-api + + + 17 + 2.2.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.ssssssss + magic-api-spring-boot-starter + ${magic-api.version} + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/livestock/trading/LivestockTradingApplication.java b/src/main/java/com/livestock/trading/LivestockTradingApplication.java new file mode 100644 index 0000000..5476e44 --- /dev/null +++ b/src/main/java/com/livestock/trading/LivestockTradingApplication.java @@ -0,0 +1,12 @@ +package com.livestock.trading; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LivestockTradingApplication { + + public static void main(String[] args) { + SpringApplication.run(LivestockTradingApplication.class, args); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..953ec83 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,33 @@ +server: + port: 8080 + +spring: + application: + name: livestock-trading-backend + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://123.57.211.165:15432/ry-vue + username: postgres + password: admin123 + +magic-api: + web: /magic/web + resource: + location: ./data/magic-api + show-sql: true + sql-column-case: camel + response: |- + { + code: code, + message: message, + data, + timestamp, + executeTime + } + response-code: + success: 1 + invalid: 0 + exception: -1 + security: + username: admin + password: admin123 diff --git a/src/test/java/com/livestock/trading/LivestockTradingApplicationTests.java b/src/test/java/com/livestock/trading/LivestockTradingApplicationTests.java new file mode 100644 index 0000000..2db2b92 --- /dev/null +++ b/src/test/java/com/livestock/trading/LivestockTradingApplicationTests.java @@ -0,0 +1,12 @@ +package com.livestock.trading; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LivestockTradingApplicationTests { + + @Test + void contextLoads() { + } +}