main
commit
b503d19ef6
@ -0,0 +1,7 @@ |
||||
target/ |
||||
.idea/ |
||||
*.iml |
||||
.classpath |
||||
.project |
||||
.settings/ |
||||
.DS_Store |
||||
@ -0,0 +1,14 @@ |
||||
{ |
||||
"properties" : { }, |
||||
"id" : "f8e3d2c1b0a94e5f8a7b6c5d4e3f2a1", |
||||
"name" : "大屏数据", |
||||
"type" : "api", |
||||
"parentId" : "0", |
||||
"path" : "/dashboard", |
||||
"createTime" : 1780876800000, |
||||
"updateTime" : null, |
||||
"createBy" : "admin", |
||||
"updateBy" : null, |
||||
"paths" : [ ], |
||||
"options" : [ ] |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
} |
||||
@ -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' |
||||
} |
||||
} |
||||
@ -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') |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,14 @@ |
||||
{ |
||||
"properties" : { }, |
||||
"id" : "d9addaf6c9904669b19434d023e8e55e", |
||||
"name" : "测试分组", |
||||
"type" : "api", |
||||
"parentId" : "0", |
||||
"path" : "/test", |
||||
"createTime" : 1780801394664, |
||||
"updateTime" : null, |
||||
"createBy" : "admin", |
||||
"updateBy" : null, |
||||
"paths" : [ ], |
||||
"options" : [ ] |
||||
} |
||||
@ -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' |
||||
@ -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 |
||||
@ -0,0 +1,59 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<parent> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-parent</artifactId> |
||||
<version>3.4.5</version> |
||||
<relativePath/> |
||||
</parent> |
||||
|
||||
<groupId>com.livestock</groupId> |
||||
<artifactId>livestock-trading-backend</artifactId> |
||||
<version>1.0.0-SNAPSHOT</version> |
||||
<name>livestock-trading-backend</name> |
||||
<description>Livestock trading backend based on magic-api</description> |
||||
|
||||
<properties> |
||||
<java.version>17</java.version> |
||||
<magic-api.version>2.2.2</magic-api.version> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-web</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-jdbc</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.ssssssss</groupId> |
||||
<artifactId>magic-api-spring-boot-starter</artifactId> |
||||
<version>${magic-api.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.postgresql</groupId> |
||||
<artifactId>postgresql</artifactId> |
||||
<scope>runtime</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-test</artifactId> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
|
||||
<build> |
||||
<plugins> |
||||
<plugin> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-maven-plugin</artifactId> |
||||
</plugin> |
||||
</plugins> |
||||
</build> |
||||
</project> |
||||
@ -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); |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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() { |
||||
} |
||||
} |
||||
Loading…
Reference in new issue