feat: add device treatment rules management and custom parameter configuration modules
This commit is contained in:
1 parent
0dbaaac397
commit
aed405743a
21 files changed
+2042
-27
No files matched your search
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# UniAI Health (原愈极刻) WSL-Ubuntu 本地验证环境一键初始化脚本
|
||||
# 适用环境:WSL2 Ubuntu 22.04 / 24.04 LTS
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
echo -e "${BLUE}==================================================================${NC}"
|
||||
echo -e "${BLUE} UniAI Health (原愈极刻) - WSL 验证环境搭建工具 ${NC}"
|
||||
echo -e "${BLUE}==================================================================${NC}"
|
||||
echo -e "项目路径: ${PROJECT_ROOT}"
|
||||
|
||||
# 1. 检查基础依赖 (Java 17 / Maven / Python3)
|
||||
echo -e "\n${YELLOW}[1/5] 检查 Java、Maven 与 Python 环境...${NC}"
|
||||
if command -v java >/dev/null 2>&1; then
|
||||
JAVA_VER=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
|
||||
echo -e "${GREEN}✓ 检测到 Java: ${JAVA_VER}${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ 未检测到 Java,正在通过 apt 安装 OpenJDK 17...${NC}"
|
||||
sudo apt-get update && sudo apt-get install -y openjdk-17-jdk
|
||||
fi
|
||||
|
||||
if command -v mvn >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ 检测到 Maven: $(mvn -v | head -n 1)${NC}"
|
||||
else
|
||||
if [ -f "/mnt/d/apache/apache-maven-3.9.9/bin/mvn" ]; then
|
||||
echo -e "${GREEN}✓ 关联本地 Maven 路径至 /usr/local/bin/mvn${NC}"
|
||||
sudo ln -sf /mnt/d/apache/apache-maven-3.9.9/bin/mvn /usr/local/bin/mvn
|
||||
else
|
||||
echo -e "${RED}正在通过 apt 安装 maven...${NC}"
|
||||
sudo apt-get install -y maven
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ 检测到 Python3: $(python3 --version)${NC}"
|
||||
else
|
||||
sudo apt-get install -y python3 python3-pip
|
||||
fi
|
||||
|
||||
# 2. 安装与配置本地 MySQL 8.0 & Redis
|
||||
echo -e "\n${YELLOW}[2/5] 检查与安装本地 MySQL & Redis 服务...${NC}"
|
||||
|
||||
# 安装与启动 MySQL
|
||||
if ! command -v mysql >/dev/null 2>&1; then
|
||||
echo -e "正在通过 apt 安装 mysql-server..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mysql-server
|
||||
fi
|
||||
|
||||
sudo service mysql status >/dev/null 2>&1 || sudo service mysql start
|
||||
echo -e "${GREEN}✓ MySQL 服务已就绪 (端口 3306)${NC}"
|
||||
|
||||
# 安装与启动 Redis
|
||||
if ! command -v redis-server >/dev/null 2>&1; then
|
||||
echo -e "正在安装 redis-server..."
|
||||
sudo apt-get install -y redis-server
|
||||
fi
|
||||
|
||||
sudo service redis-server status >/dev/null 2>&1 || sudo service redis-server start
|
||||
echo -e "${GREEN}✓ Redis 服务已就绪 (端口 6379)${NC}"
|
||||
|
||||
# 3. 初始化 MySQL 数据库、用户及数据表
|
||||
echo -e "\n${YELLOW}[3/5] 初始化测试数据库 uni_ai_doctor 及权限...${NC}"
|
||||
|
||||
sudo mysql <<EOF
|
||||
CREATE DATABASE IF NOT EXISTS \`uni_ai_doctor\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS 'unizk'@'%' IDENTIFIED BY 'Unizk@2025';
|
||||
CREATE USER IF NOT EXISTS 'unizk'@'localhost' IDENTIFIED BY 'Unizk@2025';
|
||||
ALTER USER 'unizk'@'%' IDENTIFIED BY 'Unizk@2025';
|
||||
ALTER USER 'unizk'@'localhost' IDENTIFIED BY 'Unizk@2025';
|
||||
GRANT ALL PRIVILEGES ON \`uni_ai_doctor\`.* TO 'unizk'@'%';
|
||||
GRANT ALL PRIVILEGES ON \`uni_ai_doctor\`.* TO 'unizk'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ 数据库及专用用户 unizk 初始化完成${NC}"
|
||||
|
||||
# 4. 导入验证表结构与初始种子数据
|
||||
echo -e "\n${YELLOW}[4/5] 导入定制治疗参数数据表与基准测试数据...${NC}"
|
||||
|
||||
SEED_SQL="${PROJECT_ROOT}/docs/sql/init_verification_seed_data.sql"
|
||||
if [ -f "${SEED_SQL}" ]; then
|
||||
mysql -uunizk -p'Unizk@2025' uni_ai_doctor < "${SEED_SQL}"
|
||||
echo -e "${GREEN}✓ 验证表结构与测试种子数据导入成功!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}执行定制参数 DDL...${NC}"
|
||||
mysql -uunizk -p'Unizk@2025' uni_ai_doctor < "${PROJECT_ROOT}/docs/sql/20261019_device_custom_treatment_parameters.sql"
|
||||
fi
|
||||
|
||||
# 5. 完成提示
|
||||
echo -e "\n${BLUE}==================================================================${NC}"
|
||||
echo -e "${GREEN}恭喜!WSL-Ubuntu 本地验证环境已就绪!${NC}"
|
||||
echo -e "${BLUE}==================================================================${NC}"
|
||||
echo -e "您可以通过以下方式启动并验证服务:"
|
||||
echo -e ""
|
||||
echo -e " 1. 编译并启动后端 (在 WSL 中执行):"
|
||||
echo -e " ${YELLOW}cd ${PROJECT_ROOT}/server && mvn spring-boot:run -pl admin -Dspring-boot.run.profiles=local${NC}"
|
||||
echo -e ""
|
||||
echo -e " 2. 运行一键端到端功能验证脚本 (验证定制参数及 UDP 调度):"
|
||||
echo -e " ${YELLOW}python3 ${PROJECT_ROOT}/tools/test_verify_custom_params.py${NC}"
|
||||
echo -e ""
|
||||
echo -e " 3. 启动管理后台前端 (在 Windows 或 WSL 中均可):"
|
||||
echo -e " ${YELLOW}cd ${PROJECT_ROOT}/web/admin && npm run dev${NC}"
|
||||
echo -e "${BLUE}==================================================================${NC}"
|
||||
@@ -0,0 +1,223 @@
|
||||
-- ==============================================================================
|
||||
-- UniAI Health 验证环境专用初始表结构与测试种子数据
|
||||
-- ==============================================================================
|
||||
|
||||
USE `uni_ai_doctor`;
|
||||
|
||||
-- 1. 设备表 (device)
|
||||
CREATE TABLE IF NOT EXISTS `device` (
|
||||
`device_id` VARCHAR(64) NOT NULL COMMENT '设备ID',
|
||||
`muid` VARCHAR(64) DEFAULT NULL COMMENT 'MUID硬件识别码',
|
||||
`store_id` INT DEFAULT NULL COMMENT '所属门店ID',
|
||||
`agent_id` INT DEFAULT NULL,
|
||||
`user_id` INT DEFAULT NULL,
|
||||
`status` VARCHAR(10) DEFAULT '1' COMMENT '设备状态 1-启用 0-禁用',
|
||||
`affiliated_with` VARCHAR(255) DEFAULT '测试机构',
|
||||
`payment_method` INT DEFAULT 1,
|
||||
`expire_time` VARCHAR(64) DEFAULT '999',
|
||||
`warehousing_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`outbound_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`balance` DECIMAL(10,2) DEFAULT '0.00',
|
||||
`surplus` INT DEFAULT 999 COMMENT '剩余使用次数',
|
||||
`force_pay` INT DEFAULT 0,
|
||||
`lo` VARCHAR(64) DEFAULT '114.05',
|
||||
`la` VARCHAR(64) DEFAULT '22.54',
|
||||
`free_count` INT DEFAULT 0,
|
||||
`upgrade_switch` INT DEFAULT 0,
|
||||
`operating_status` INT DEFAULT 3 COMMENT '运行状态:0-运行 1-关机 2-异常 3-待机',
|
||||
`last_run_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`cf` VARCHAR(10) DEFAULT '1' COMMENT '机器配置:1-医疗版本 2-非医疗版本',
|
||||
`wm` VARCHAR(10) DEFAULT '1' COMMENT '工作模式:1-联网 0-脱机',
|
||||
`hw` VARCHAR(32) DEFAULT 'V1.0',
|
||||
`sw` VARCHAR(32) DEFAULT 'V1.0',
|
||||
`iccid` VARCHAR(64) DEFAULT '89860000000000000000',
|
||||
`cr` VARCHAR(32) DEFAULT '4G_V1.0',
|
||||
`mo` VARCHAR(10) DEFAULT '3',
|
||||
`recharge_count` INT DEFAULT 0,
|
||||
`remark` VARCHAR(255) DEFAULT '测试设备',
|
||||
`treat_status` INT DEFAULT 0,
|
||||
`device_tags` VARCHAR(255) DEFAULT '' COMMENT '设备标签(逗号隔开)',
|
||||
`model_category` VARCHAR(64) DEFAULT 'DEFAULT' COMMENT '机型品类',
|
||||
PRIMARY KEY (`device_id`),
|
||||
KEY `idx_muid` (`muid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 2. 全局基准治疗参数表 (treat_parameter)
|
||||
CREATE TABLE IF NOT EXISTS `treat_parameter` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`symptoms_name` VARCHAR(128) NOT NULL COMMENT '病症名称',
|
||||
`acupoint` VARCHAR(64) DEFAULT '阿是穴' COMMENT '穴位',
|
||||
`ul` VARCHAR(10) DEFAULT '01' COMMENT '超声幅度',
|
||||
`pl` VARCHAR(10) DEFAULT '08' COMMENT '脉幅',
|
||||
`pw` VARCHAR(10) DEFAULT '50' COMMENT '脉宽',
|
||||
`fr` VARCHAR(10) DEFAULT '03' COMMENT '频率',
|
||||
`tl` VARCHAR(10) DEFAULT '017' COMMENT '时长',
|
||||
`part` VARCHAR(64) DEFAULT '膝关节' COMMENT '部位',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 3. 定制参数规则表 (device_treat_rule)
|
||||
CREATE TABLE IF NOT EXISTS `device_treat_rule` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`rule_name` VARCHAR(64) NOT NULL,
|
||||
`target_type` TINYINT NOT NULL COMMENT '1-单机, 2-标签, 3-机型',
|
||||
`target_value` VARCHAR(128) NOT NULL,
|
||||
`symptom_id` INT NOT NULL,
|
||||
`fr` VARCHAR(10) DEFAULT NULL,
|
||||
`pw` VARCHAR(10) DEFAULT NULL,
|
||||
`pl` VARCHAR(10) DEFAULT NULL,
|
||||
`ul` VARCHAR(10) DEFAULT NULL,
|
||||
`tl` VARCHAR(10) DEFAULT NULL,
|
||||
`priority` INT NOT NULL DEFAULT 10,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`remark` VARCHAR(255) DEFAULT NULL,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_symptom_target` (`symptom_id`, `target_type`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 4. 移动端用户表 (mobile_app_user)
|
||||
DROP TABLE IF EXISTS `mobile_app_user`;
|
||||
CREATE TABLE `mobile_app_user` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`openid` VARCHAR(64) DEFAULT 'test_openid',
|
||||
`phone_number` VARCHAR(32) DEFAULT '13800000000',
|
||||
`nickname` VARCHAR(64) DEFAULT '测试患者',
|
||||
`avatar` VARCHAR(255) DEFAULT '',
|
||||
`pain_number` VARCHAR(64) DEFAULT '1' COMMENT '问诊匹配的病症ID',
|
||||
`age` INT DEFAULT 30,
|
||||
`height` DOUBLE DEFAULT 170.0,
|
||||
`weight` DOUBLE DEFAULT 65.0,
|
||||
`symptom` VARCHAR(255) DEFAULT '膝关节半月板损伤',
|
||||
`points` INT DEFAULT 0,
|
||||
`has_member` INT DEFAULT 0,
|
||||
`therapy` VARCHAR(255) DEFAULT '',
|
||||
`fours_rating` INT DEFAULT 0,
|
||||
`evaluation_result` VARCHAR(255) DEFAULT '',
|
||||
`user_type` VARCHAR(10) DEFAULT 'C',
|
||||
`gender` INT DEFAULT 1,
|
||||
`free_trial` INT DEFAULT 0,
|
||||
`first_store_id` INT DEFAULT NULL,
|
||||
`first_physio_id` INT DEFAULT NULL,
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 5. 设备运行时参数记录表 (user_treatment_parameters)
|
||||
CREATE TABLE IF NOT EXISTS `user_treatment_parameters` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`app_user_id` INT DEFAULT NULL,
|
||||
`device_id` VARCHAR(64) DEFAULT NULL,
|
||||
`ul` VARCHAR(10) DEFAULT NULL,
|
||||
`pl` VARCHAR(10) DEFAULT NULL,
|
||||
`pw` VARCHAR(10) DEFAULT NULL,
|
||||
`fr` VARCHAR(10) DEFAULT NULL,
|
||||
`tl` VARCHAR(10) DEFAULT NULL,
|
||||
`status` INT DEFAULT 1,
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 6. 设备使用记录表 (device_usage_record)
|
||||
CREATE TABLE IF NOT EXISTS `device_usage_record` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) DEFAULT NULL,
|
||||
`start_time` DATETIME DEFAULT NULL,
|
||||
`end_time` DATETIME DEFAULT NULL,
|
||||
`start_use_flag` VARCHAR(10) DEFAULT 'false',
|
||||
`wm` VARCHAR(10) DEFAULT '1',
|
||||
`belong` VARCHAR(64) DEFAULT NULL,
|
||||
`muid` VARCHAR(64) DEFAULT NULL,
|
||||
`invalid_flag` INT DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 7. 用户设备理疗记录表 (user_device_treat_record)
|
||||
CREATE TABLE IF NOT EXISTS `user_device_treat_record` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT DEFAULT NULL,
|
||||
`device_id` VARCHAR(64) DEFAULT NULL,
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 8. 系统用户表 (sys_user)
|
||||
CREATE TABLE IF NOT EXISTS `sys_user` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`role` VARCHAR(32) DEFAULT 'ROLE_ADMIN',
|
||||
`store_id` INT DEFAULT NULL,
|
||||
`username` VARCHAR(64) NOT NULL,
|
||||
`password` VARCHAR(128) NOT NULL,
|
||||
`dept` VARCHAR(64) DEFAULT '管理部',
|
||||
`status` VARCHAR(16) DEFAULT 'enable',
|
||||
`avatar` VARCHAR(255) DEFAULT '',
|
||||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 9. 门店表 (jk_store)
|
||||
CREATE TABLE IF NOT EXISTS `jk_store` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`store_name` VARCHAR(128) DEFAULT '测试旗舰店',
|
||||
`store_image` VARCHAR(255) DEFAULT '',
|
||||
`address` VARCHAR(255) DEFAULT '北京市海淀区',
|
||||
`status` VARCHAR(16) DEFAULT 'enable',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ==============================================================================
|
||||
-- 插入测试种子数据
|
||||
-- ==============================================================================
|
||||
|
||||
-- 清空旧测试数据
|
||||
DELETE FROM `treat_parameter` WHERE `id` = 1;
|
||||
DELETE FROM `mobile_app_user` WHERE `id` = 9;
|
||||
DELETE FROM `device` WHERE `device_id` LIKE 'DEV_%';
|
||||
DELETE FROM `device_treat_rule` WHERE `id` IN (101, 102, 103);
|
||||
DELETE FROM `sys_user` WHERE `username` = 'admin';
|
||||
|
||||
-- (1) 基准病症参数: ID=1 膝关节半月板损伤 (默认: UL=01, PL=08, PW=50, FR=03, TL=017)
|
||||
INSERT INTO `treat_parameter` (`id`, `symptoms_name`, `acupoint`, `ul`, `pl`, `pw`, `fr`, `tl`, `part`)
|
||||
VALUES (1, '膝关节半月板损伤', '膝眼/鹤顶', '01', '08', '50', '03', '017', '膝关节');
|
||||
|
||||
-- (2) 默认测试用户: ID=9, 当前问诊病症 pain_number = 1
|
||||
INSERT INTO `mobile_app_user` (`id`, `openid`, `phone_number`, `nickname`, `pain_number`, `user_type`)
|
||||
VALUES (9, 'oTestUser009', '13800138000', '张三(测试理疗)', '1', 'C');
|
||||
|
||||
-- (3) 4 台不同维度的测试设备
|
||||
-- 设备A: 打上 golf_vip 标签的设备 (测试标签定制覆盖)
|
||||
INSERT INTO `device` (`device_id`, `muid`, `cf`, `device_tags`, `status`, `operating_status`, `surplus`)
|
||||
VALUES ('DEV_GOLF_01', 'DEV_GOLF_01', '1', 'golf_vip,vip_care', '1', 3, 999);
|
||||
|
||||
-- 设备B: 单机 DEV_SPEC_02 (测试单机专属定制覆盖)
|
||||
INSERT INTO `device` (`device_id`, `muid`, `cf`, `device_tags`, `status`, `operating_status`, `surplus`)
|
||||
VALUES ('DEV_SPEC_02', 'DEV_SPEC_02', '1', 'elderly_care', '1', 3, 999);
|
||||
|
||||
-- 设备C: 非医疗版本民用机 cf=2 (测试安全上限截断 UL <= 10)
|
||||
INSERT INTO `device` (`device_id`, `muid`, `cf`, `device_tags`, `status`, `operating_status`, `surplus`)
|
||||
VALUES ('DEV_CIVIL_03', 'DEV_CIVIL_03', '2', '', '1', 3, 999);
|
||||
|
||||
-- 设备D: 普通未打标签的医疗设备 (测试基准默认参数兜底)
|
||||
INSERT INTO `device` (`device_id`, `muid`, `cf`, `device_tags`, `status`, `operating_status`, `surplus`)
|
||||
VALUES ('DEV_DEF_04', 'DEV_DEF_04', '1', '', '1', 3, 999);
|
||||
|
||||
-- (4) 3 条定制规则
|
||||
-- 规则1: 针对标签 [golf_vip],病症1 定制高能高频 (FR=08, PW=55, PL=12, UL=05, TL=025)
|
||||
INSERT INTO `device_treat_rule` (`id`, `rule_name`, `target_type`, `target_value`, `symptom_id`, `fr`, `pw`, `pl`, `ul`, `tl`, `priority`, `status`)
|
||||
VALUES (101, '高尔夫专享定制', 2, 'golf_vip', 1, '08', '55', '12', '05', '025', 10, 1);
|
||||
|
||||
-- 规则2: 针对单机 [DEV_SPEC_02],病症1 定制温和轻柔 (FR=02, PW=40, PL=04, UL=02, TL=010)
|
||||
INSERT INTO `device_treat_rule` (`id`, `rule_name`, `target_type`, `target_value`, `symptom_id`, `fr`, `pw`, `pl`, `ul`, `tl`, `priority`, `status`)
|
||||
VALUES (102, '特定设备单机特护', 1, 'DEV_SPEC_02', 1, '02', '40', '04', '02', '010', 50, 1);
|
||||
|
||||
-- 规则3: 针对机型配置 [2] (非医疗民用版),将 UL 故意配置为过高值 18 (验证安全引擎自动截断至 10)
|
||||
INSERT INTO `device_treat_rule` (`id`, `rule_name`, `target_type`, `target_value`, `symptom_id`, `fr`, `pw`, `pl`, `ul`, `tl`, `priority`, `status`)
|
||||
VALUES (103, '非医疗版超标规则', 3, '2', 1, '04', '45', '09', '18', '015', 5, 1);
|
||||
|
||||
-- (5) 系统管理员用户: admin / admin123 (BCrypt hash)
|
||||
INSERT INTO `sys_user` (`id`, `username`, `password`, `role`, `status`)
|
||||
VALUES (1, 'admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'ROLE_ADMIN', 'enable');
|
||||
@@ -0,0 +1,170 @@
|
||||
package cn.unizk.controller;
|
||||
|
||||
import cn.unizk.entity.po.DeviceTreatRule;
|
||||
import cn.unizk.entity.po.TreatParameter;
|
||||
import cn.unizk.entity.vo.ResultData;
|
||||
import cn.unizk.entity.vo.UniPage;
|
||||
import cn.unizk.exception.ServiceException;
|
||||
import cn.unizk.mapper.DeviceTreatRuleMapper;
|
||||
import cn.unizk.mapper.TreatParameterMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 设备治疗参数定制规则控制器
|
||||
*
|
||||
* @author hao / antigravity
|
||||
* @date 2026/10/19
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/pc/admin/deviceTreatRule")
|
||||
public class DeviceTreatRuleController {
|
||||
|
||||
@Resource
|
||||
private DeviceTreatRuleMapper deviceTreatRuleMapper;
|
||||
|
||||
@Resource
|
||||
private TreatParameterMapper treatParameterMapper;
|
||||
|
||||
/**
|
||||
* 分页查询定制规则
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public Page<DeviceTreatRule> page(UniPage<DeviceTreatRule> uniPage,
|
||||
@RequestParam(value = "ruleName", required = false) String ruleName,
|
||||
@RequestParam(value = "targetType", required = false) Integer targetType,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "symptomId", required = false) Integer symptomId) {
|
||||
Page<DeviceTreatRule> page = new Page<>(uniPage.getCurrentPage(), uniPage.getPageSize());
|
||||
LambdaQueryWrapper<DeviceTreatRule> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.like(StringUtils.isNotBlank(ruleName), DeviceTreatRule::getRuleName, ruleName)
|
||||
.eq(targetType != null, DeviceTreatRule::getTargetType, targetType)
|
||||
.eq(status != null, DeviceTreatRule::getStatus, status)
|
||||
.eq(symptomId != null, DeviceTreatRule::getSymptomId, symptomId)
|
||||
.orderByDesc(DeviceTreatRule::getPriority)
|
||||
.orderByDesc(DeviceTreatRule::getId);
|
||||
|
||||
Page<DeviceTreatRule> resultPage = deviceTreatRuleMapper.selectPage(page, queryWrapper);
|
||||
List<DeviceTreatRule> records = resultPage.getRecords();
|
||||
if (CollectionUtils.isNotEmpty(records)) {
|
||||
List<Integer> symptomIds = records.stream()
|
||||
.map(DeviceTreatRule::getSymptomId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollectionUtils.isNotEmpty(symptomIds)) {
|
||||
List<TreatParameter> treatParameters = treatParameterMapper.selectBatchIds(symptomIds);
|
||||
Map<Integer, String> symptomMap = treatParameters.stream()
|
||||
.collect(Collectors.toMap(TreatParameter::getId, TreatParameter::getSymptomsName, (k1, k2) -> k1));
|
||||
records.forEach(r -> r.setSymptomsName(symptomMap.get(r.getSymptomId())));
|
||||
}
|
||||
}
|
||||
return resultPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或更新定制规则
|
||||
*/
|
||||
@PostMapping("/saveOrUpdate")
|
||||
public ResultData saveOrUpdate(@RequestBody DeviceTreatRule rule) {
|
||||
if (StringUtils.isBlank(rule.getRuleName())) {
|
||||
throw new ServiceException("规则名称不能为空");
|
||||
}
|
||||
if (rule.getTargetType() == null || rule.getTargetType() < 1 || rule.getTargetType() > 3) {
|
||||
throw new ServiceException("适用类型不正确(1:单机 2:标签 3:机型)");
|
||||
}
|
||||
if (StringUtils.isBlank(rule.getTargetValue())) {
|
||||
throw new ServiceException("匹配目标标识不能为空");
|
||||
}
|
||||
if (rule.getSymptomId() == null) {
|
||||
throw new ServiceException("关联病症不能为空");
|
||||
}
|
||||
if (rule.getPriority() == null) {
|
||||
rule.setPriority(10);
|
||||
}
|
||||
if (rule.getStatus() == null) {
|
||||
rule.setStatus(1);
|
||||
}
|
||||
|
||||
// 参数长度标准化(两字符数字)
|
||||
rule.setFr(formatParam(rule.getFr(), true));
|
||||
rule.setPl(formatParam(rule.getPl(), true));
|
||||
rule.setPw(formatParam(rule.getPw(), true));
|
||||
rule.setUl(formatParam(rule.getUl(), true));
|
||||
rule.setTl(formatParam(rule.getTl(), false));
|
||||
|
||||
if (rule.getId() == null) {
|
||||
rule.setCreateTime(LocalDateTime.now());
|
||||
rule.setUpdateTime(LocalDateTime.now());
|
||||
deviceTreatRuleMapper.insert(rule);
|
||||
} else {
|
||||
rule.setUpdateTime(LocalDateTime.now());
|
||||
deviceTreatRuleMapper.updateById(rule);
|
||||
}
|
||||
return ResultData.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定制规则
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
public ResultData delete(@RequestParam("id") Integer id) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("ID不能为空");
|
||||
}
|
||||
return ResultData.success(deviceTreatRuleMapper.deleteById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用规则
|
||||
*/
|
||||
@PostMapping("/changeStatus")
|
||||
public ResultData changeStatus(@RequestParam("id") Integer id, @RequestParam("status") Integer status) {
|
||||
if (id == null || status == null) {
|
||||
throw new ServiceException("参数不能为空");
|
||||
}
|
||||
DeviceTreatRule rule = new DeviceTreatRule();
|
||||
rule.setId(id);
|
||||
rule.setStatus(status);
|
||||
rule.setUpdateTime(LocalDateTime.now());
|
||||
return ResultData.success(deviceTreatRuleMapper.updateById(rule));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
@GetMapping("/getOne")
|
||||
public ResultData<DeviceTreatRule> getOne(@RequestParam("id") Integer id) {
|
||||
DeviceTreatRule rule = deviceTreatRuleMapper.selectById(id);
|
||||
if (rule != null && rule.getSymptomId() != null) {
|
||||
TreatParameter treatParameter = treatParameterMapper.selectById(rule.getSymptomId());
|
||||
if (treatParameter != null) {
|
||||
rule.setSymptomsName(treatParameter.getSymptomsName());
|
||||
}
|
||||
}
|
||||
return ResultData.success(rule);
|
||||
}
|
||||
|
||||
private String formatParam(String p, boolean padZero) {
|
||||
if (StringUtils.isBlank(p)) {
|
||||
return null;
|
||||
}
|
||||
p = p.trim();
|
||||
if (padZero && p.length() == 1) {
|
||||
return "0" + p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -425,6 +425,39 @@ public class SysDeviceController {
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/setTags")
|
||||
public boolean setTags(@RequestBody DeviceTagsVO vo) {
|
||||
List<String> deviceIds = vo.getDeviceIds();
|
||||
if (CollectionUtils.isEmpty(deviceIds)) {
|
||||
return false;
|
||||
}
|
||||
List<Device> existingDevices = deviceService.listByIds(deviceIds);
|
||||
List<Device> updateList = new ArrayList<>();
|
||||
for (Device dev : existingDevices) {
|
||||
Device updateDevice = new Device();
|
||||
updateDevice.setDeviceId(dev.getDeviceId());
|
||||
if (vo.getMode() != null && vo.getMode() == 2) {
|
||||
String currentTags = dev.getDeviceTags();
|
||||
if (StringUtils.isBlank(currentTags)) {
|
||||
updateDevice.setDeviceTags(vo.getTags() == null ? "" : vo.getTags());
|
||||
} else {
|
||||
Set<String> tagSet = new LinkedHashSet<>(Arrays.asList(currentTags.split(",")));
|
||||
if (StringUtils.isNotBlank(vo.getTags())) {
|
||||
tagSet.addAll(Arrays.asList(vo.getTags().split(",")));
|
||||
}
|
||||
updateDevice.setDeviceTags(String.join(",", tagSet));
|
||||
}
|
||||
} else {
|
||||
updateDevice.setDeviceTags(vo.getTags() == null ? "" : vo.getTags());
|
||||
}
|
||||
if (StringUtils.isNotBlank(vo.getModelCategory())) {
|
||||
updateDevice.setModelCategory(vo.getModelCategory());
|
||||
}
|
||||
updateList.add(updateDevice);
|
||||
}
|
||||
return deviceService.updateBatchById(updateList);
|
||||
}
|
||||
|
||||
@PostMapping("/upgrade")
|
||||
public ResultData upgrade(@RequestParam(value = "id",required = false) Integer id,
|
||||
@RequestParam(value = "file",required = false) MultipartFile file,
|
||||
|
||||
@@ -1,68 +1,210 @@
|
||||
package cn.unizk.service;
|
||||
|
||||
import cn.unizk.entity.dto.PrDTO;
|
||||
import cn.unizk.entity.po.Device;
|
||||
import cn.unizk.entity.po.DeviceTreatRule;
|
||||
import cn.unizk.entity.po.MobileAppUser;
|
||||
import cn.unizk.entity.po.TreatParameter;
|
||||
import cn.unizk.mapper.DeviceMapper;
|
||||
import cn.unizk.mapper.DeviceTreatRuleMapper;
|
||||
import cn.unizk.mapper.MobileAppUserMapper;
|
||||
import cn.unizk.mapper.TreatParameterMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @Author hao
|
||||
* @Date 2025/12/18 17:00
|
||||
* @Version 1.0
|
||||
* 治疗处方参数生成引擎(支持单机、标签、机型品类分层覆盖)
|
||||
*
|
||||
* @author hao / antigravity
|
||||
* @date 2026/10/19
|
||||
**/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GeneratePrService {
|
||||
|
||||
@Resource
|
||||
private MobileAppUserMapper mobileAppUserMapper;
|
||||
|
||||
@Resource
|
||||
private TreatParameterMapper treatParameterMapper;
|
||||
|
||||
public PrDTO generatePr(Integer userId){
|
||||
if (userId == null){
|
||||
@Resource
|
||||
private DeviceMapper deviceMapper;
|
||||
|
||||
@Resource
|
||||
private DeviceTreatRuleMapper deviceTreatRuleMapper;
|
||||
|
||||
/**
|
||||
* 兼容旧接口:无设备上下文时使用全局基准参数
|
||||
*/
|
||||
public PrDTO generatePr(Integer userId) {
|
||||
return generatePr(userId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新接口:传入具体设备,应用分层定制覆盖算法
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param deviceId 设备ID
|
||||
* @return 处方DTO
|
||||
*/
|
||||
public PrDTO generatePr(Integer userId, String deviceId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
MobileAppUser user = mobileAppUserMapper.selectById(userId);
|
||||
if (user == null || StringUtils.isBlank(user.getPainNumber())){
|
||||
if (user == null || StringUtils.isBlank(user.getPainNumber())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Device device = StringUtils.isNotBlank(deviceId) ? deviceMapper.selectById(deviceId) : null;
|
||||
|
||||
PrDTO prDTO = new PrDTO();
|
||||
String uuid = RandomStringUtils.randomNumeric(16);
|
||||
prDTO.setId(uuid);
|
||||
List<PrDTO.PrItem> prItems = new ArrayList<>();
|
||||
String painNumber = user.getPainNumber();
|
||||
if (painNumber.contains(",")){
|
||||
if (painNumber.contains(",")) {
|
||||
String[] split = painNumber.split(",");
|
||||
for (String s : split) {
|
||||
PrDTO.PrItem prItem = fill(s);
|
||||
PrDTO.PrItem prItem = fill(s.trim(), device);
|
||||
if (prItem != null) {
|
||||
prItems.add(prItem);
|
||||
}
|
||||
prDTO.setPrItems(prItems);
|
||||
}else {
|
||||
PrDTO.PrItem prItem = fill(painNumber);
|
||||
prItems.add(prItem);
|
||||
prDTO.setPrItems(prItems);
|
||||
}
|
||||
} else {
|
||||
PrDTO.PrItem prItem = fill(painNumber.trim(), device);
|
||||
if (prItem != null) {
|
||||
prItems.add(prItem);
|
||||
}
|
||||
}
|
||||
prDTO.setPrItems(prItems);
|
||||
return prDTO;
|
||||
}
|
||||
|
||||
private PrDTO.PrItem fill(String painNumber){
|
||||
TreatParameter treatParameter = treatParameterMapper.selectById(Integer.parseInt(painNumber));
|
||||
/**
|
||||
* 解析单个病症项的参数
|
||||
*/
|
||||
private PrDTO.PrItem fill(String painNumber, Device device) {
|
||||
if (StringUtils.isBlank(painNumber)) {
|
||||
return null;
|
||||
}
|
||||
Integer symptomId = Integer.parseInt(painNumber);
|
||||
TreatParameter baseline = treatParameterMapper.selectById(symptomId);
|
||||
if (baseline == null) {
|
||||
log.warn("未找到病症编号为 {} 的基准治疗参数", painNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
PrDTO.PrItem prItem = new PrDTO.PrItem();
|
||||
prItem.setFr(treatParameter.getFr());
|
||||
prItem.setPl(treatParameter.getPl());
|
||||
prItem.setPw(treatParameter.getPw());
|
||||
prItem.setUl(treatParameter.getUl());
|
||||
prItem.setTl(treatParameter.getTl());
|
||||
// 1. 基准参数兜底
|
||||
prItem.setFr(baseline.getFr());
|
||||
prItem.setPl(baseline.getPl());
|
||||
prItem.setPw(baseline.getPw());
|
||||
prItem.setUl(baseline.getUl());
|
||||
prItem.setTl(baseline.getTl());
|
||||
|
||||
if (device == null) {
|
||||
return prItem;
|
||||
}
|
||||
|
||||
// 2. 查询该病症适用的定制规则(状态为启用)
|
||||
List<DeviceTreatRule> rules = deviceTreatRuleMapper.selectList(
|
||||
new LambdaQueryWrapper<DeviceTreatRule>()
|
||||
.eq(DeviceTreatRule::getSymptomId, symptomId)
|
||||
.eq(DeviceTreatRule::getStatus, 1)
|
||||
);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(rules)) {
|
||||
DeviceTreatRule bestRule = findBestMatchedRule(rules, device);
|
||||
if (bestRule != null) {
|
||||
log.info("设备[{}]命中定制治疗参数规则:[{}], 规则ID:{}",
|
||||
device.getDeviceId(), bestRule.getRuleName(), bestRule.getId());
|
||||
// 非空字段覆盖基准
|
||||
if (StringUtils.isNotBlank(bestRule.getFr())) prItem.setFr(bestRule.getFr());
|
||||
if (StringUtils.isNotBlank(bestRule.getPl())) prItem.setPl(bestRule.getPl());
|
||||
if (StringUtils.isNotBlank(bestRule.getPw())) prItem.setPw(bestRule.getPw());
|
||||
if (StringUtils.isNotBlank(bestRule.getUl())) prItem.setUl(bestRule.getUl());
|
||||
if (StringUtils.isNotBlank(bestRule.getTl())) prItem.setTl(bestRule.getTl());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 硬件物理安全底线校验与截断
|
||||
applySafetyLimits(prItem, device);
|
||||
|
||||
return prItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则匹配器:按 单机(Type 1) > 标签(Type 2) > 机型品类(Type 3) 结合优先级权重挑选最优规则
|
||||
*/
|
||||
private DeviceTreatRule findBestMatchedRule(List<DeviceTreatRule> rules, Device device) {
|
||||
List<String> tags = Collections.emptyList();
|
||||
if (StringUtils.isNotBlank(device.getDeviceTags())) {
|
||||
tags = Arrays.asList(device.getDeviceTags().split(","));
|
||||
}
|
||||
|
||||
List<DeviceTreatRule> matchedList = new ArrayList<>();
|
||||
for (DeviceTreatRule rule : rules) {
|
||||
if (rule.getTargetType() == null || StringUtils.isBlank(rule.getTargetValue())) {
|
||||
continue;
|
||||
}
|
||||
// 目标类型1:单机精确匹配 deviceId
|
||||
if (rule.getTargetType() == 1 && rule.getTargetValue().equalsIgnoreCase(device.getDeviceId())) {
|
||||
matchedList.add(rule);
|
||||
continue;
|
||||
}
|
||||
// 目标类型2:设备标签匹配
|
||||
if (rule.getTargetType() == 2 && tags.contains(rule.getTargetValue())) {
|
||||
matchedList.add(rule);
|
||||
continue;
|
||||
}
|
||||
// 目标类型3:机型/配置匹配(匹配 cf 如 "1"医疗/"2"非医疗,或 modelCategory)
|
||||
if (rule.getTargetType() == 3) {
|
||||
if (rule.getTargetValue().equalsIgnoreCase(device.getCf())
|
||||
|| (StringUtils.isNotBlank(device.getModelCategory()) && rule.getTargetValue().equalsIgnoreCase(device.getModelCategory()))) {
|
||||
matchedList.add(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 排序规则:单机(3000分) > 标签(2000分) > 机型(1000分) + priority数值
|
||||
return matchedList.stream().max(Comparator.comparingInt(r -> {
|
||||
int typeWeight = 0;
|
||||
if (r.getTargetType() == 1) typeWeight = 3000;
|
||||
else if (r.getTargetType() == 2) typeWeight = 2000;
|
||||
else if (r.getTargetType() == 3) typeWeight = 1000;
|
||||
int priority = r.getPriority() != null ? r.getPriority() : 0;
|
||||
return typeWeight + priority;
|
||||
})).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬件物理安全极限保护
|
||||
*/
|
||||
private void applySafetyLimits(PrDTO.PrItem item, Device device) {
|
||||
try {
|
||||
// 针对非医疗版本(cf = 2),强制限制超声幅度上限为 10,防止高能灼伤
|
||||
if ("2".equals(device.getCf()) && StringUtils.isNotBlank(item.getUl())) {
|
||||
int ulVal = Integer.parseInt(item.getUl());
|
||||
if (ulVal > 10) {
|
||||
log.warn("设备[{}]为非医疗版本,超声幅度[{}]超出民用合规上限10,已自动安全截断", device.getDeviceId(), ulVal);
|
||||
item.setUl("10");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("应用安全极限时数值解析异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ public class UdpServerService2 {
|
||||
// String openid = (String) redisTemplate.opsForValue().get(UniConstant.DEVICE_ID + device.getDeviceId());
|
||||
// String openid = "oATNGvnMbu6tbNgbnFZPWtJBGvFM";
|
||||
Integer appUserId = 9;
|
||||
PrDTO prDTO = generatePrService.generatePr(appUserId);
|
||||
PrDTO prDTO = generatePrService.generatePr(appUserId, device.getDeviceId());
|
||||
if (Objects.nonNull(prDTO)){
|
||||
List<PrDTO.PrItem> prItems = prDTO.getPrItems();
|
||||
if (CollectionUtils.isNotEmpty(prItems)){
|
||||
|
||||
@@ -57,6 +57,8 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
|
||||
wrapper.eq(StringUtils.isNotBlank(query.getSw()),Device::getSw,query.getSw());
|
||||
wrapper.eq(StringUtils.isNotBlank(query.getCr()),Device::getCr,query.getCr());
|
||||
wrapper.eq(query.getUpgradeSwitch() != null,Device::getUpgradeSwitch,query.getUpgradeSwitch());
|
||||
wrapper.like(StringUtils.isNotBlank(query.getDeviceTags()),Device::getDeviceTags,query.getDeviceTags());
|
||||
wrapper.eq(StringUtils.isNotBlank(query.getModelCategory()),Device::getModelCategory,query.getModelCategory());
|
||||
wrapper.and(hasKeyword, w -> {
|
||||
w.eq(hasKeyword, Device::getMuid, query.getKeyword())
|
||||
.or()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
druid:
|
||||
url: jdbc:mysql://127.0.0.1:3306/uni_ai_doctor?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true
|
||||
username: unizk
|
||||
password: Unizk@2025
|
||||
|
||||
redis:
|
||||
port: 6379
|
||||
host: 127.0.0.1
|
||||
username:
|
||||
password:
|
||||
database: 0
|
||||
timeout: 5000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 6
|
||||
min-idle: 2
|
||||
max-wait: -1ms
|
||||
shutdown-timeout: 360
|
||||
|
||||
token:
|
||||
header: jkzy-auth
|
||||
expireTime: 60
|
||||
|
||||
security:
|
||||
enabled: false
|
||||
|
||||
minio:
|
||||
endpoint: http://127.0.0.1:8002
|
||||
accessKey: mock
|
||||
secretKey: mock
|
||||
username: jkzy
|
||||
password: jkzy@2025
|
||||
nginxHost:
|
||||
|
||||
xf:
|
||||
appid: 3fa7b7a0
|
||||
long-sound:
|
||||
keySecret: b44d730380dcfae02313dcea7101efc8
|
||||
instant:
|
||||
host: https://iat-api.xfyun.cn/v2/iat
|
||||
APISecret: N2Q5Y2NiOTZmNThhODZjYTgxMjg1NjA4
|
||||
apiKey: da390c58ef0f2ad8424c3dccc6d458c7
|
||||
|
||||
aliyun:
|
||||
accessKeyId: LTAI5tBQGeqmhe6yhfqupaZD
|
||||
accessKeySecret: YM2lsEtpjCLOF2kJGdsCBcwRHLX8aa
|
||||
|
||||
wx:
|
||||
appid: wx71e559ade446f09a
|
||||
secret: 00b7f3cff0f4bde07daacd0dea7548c9
|
||||
redirectUrl: http://369.peaktcm.com/wx/authCallback
|
||||
|
||||
hf:
|
||||
huifuId: 6666000179463008
|
||||
payApi: https://api.huifu.com/v3/trade/payment/jspay
|
||||
refundApi: https://api.huifu.com/v3/trade/payment/scanpay/refund
|
||||
publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IcOEDMBOiHaa5dLsPWVVx+9yE/VEGjRSVZFUAospbCEhGDimE7UEL8ecXwbG9AN5k8yX+Ltj6zaeLIt3KgxbX5/4pnrOoPqFhtaoNuQrwl8/9/rkgGkoksH9e//mRan2+TjbHLKXWZKUduksaoWw4VehOx0nxE+wx7Bk7/VQAegnrDz4z/8zEd2Mcm51THceHJ5wVoMoXvo5e8nhUUAFJeHtlI5jxC85lK6kz2UNhGQ7Kutc7sBzK1U/mi29j/iStN3Z0fzND7+nRfgWPu4b0cKNnNwlIaGiabFTbpVZyV8LTg1BandmHJLNUUJyrc/s3W72VFUdp+BwgoERDcEwwIDAQAB
|
||||
privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDchw4QMwE6Idprl0uw9ZVXH73IT9UQaNFJVkVQCiylsISEYOKYTtQQvx5xfBsb0A3mTzJf4u2PrNp4si3cqDFtfn/imes6g+oWG1qg25CvCXz/3+uSAaSiSwf17/+ZFqfb5ONscspdZkpR26SxqhbDhV6E7HSfET7DHsGTv9VAB6CesPPjP/zMR3YxybnVMdx4cnnBWgyhe+jl7yeFRQAUl4e2UjmPELzmUrqTPZQ2EZDsq61zuwHMrVT+aLb2P+JK03dnR/M0Pv6dF+BY+7hvRwo2c3CUhoaJpsVNulVnJXwtODUFqd2Ycks1RQnKtz+zdbvZUVR2n4HCCgRENwTDAgMBAAECggEAbFWAaB7WVHt69G3fxi/uzF0Qe04UijojzhYueLJ22nKLtwhtaIsZDJ0G1JM2MgSUP3badVZig7lTngVSGFsb5QFGy8f3Ucip/3AypmEWg5ZyrLSQ4ioieYPIhl3/P3Jsxc58lrMRjK6KhgoTrx+yjRyAoWYTqGztWydJy05q8UekdbIn9K2Dq0294hHbarCjB3C/3SXX4xniEbDtVBTdjP81731gxnM2HJ98GDrvUf5MDrX4SdExvCYV0P0mU8uaRPK8ylWCmtsEuMxKupFUFePJNZdyG+P/RZs6CbgUgJyYUzKlOyeWH8KwQF1FLntipkIPlJ+uVkdwK2eDD6bzWQKBgQDylFiSnlFy0fwXSpWHDnDgukaLYxm2GNWaPyg0f2IqK/368o/Xu4TWOPuknR6ngeiJZz3SVA/kFeCz3+UxtDlbUheoSkkbo0cZg8PaKx875gE97oCHgRkaPS4l44kmu1ZwOMgfZDQPrLZMEbAfjqvMEa7ci5kXk1quOGeSJRIg3QKBgQDoumM2dZ+RxYxtxB2k1kJJIndZX3hhVyMfDIh5TwHHKNml1poa/s+aoT27BZgiM8snLR55xk4cQ59kr9Yic9Oa4seECsrAgP8mbB6eC/y7lRDZDFjJvHXl6a1ePo+hBlYarL83AUMIE1/SfZrdyM5yRmp3+cwHkKVAQuiEXOSHwKBgEDkpn0ylR9eRU4FydKDbnbv3gdTz4EDS8EjvCGvIheeBSeo1AomQ/b673auAZFWE1sOn18w1b9yitYTw0B0shNX9rQyizQ1jcOpmC848fYYJaTntMKLKqf2Te4c3Ifb/devmPms2LCIx4u5/5+xjwvcfybIwX+pD80jkbFq8VqRAoGBAKulTKdN+XK6EOikTHjWzezCzuPL1i105yJvS/isnNMW/e+iZ+ahKN0TL0RQy/Pt31f1nxSRfeq6tUS9q2Gdq+QAfsaUbxWsDoN4GtzWWoC/YL20rYecyBfaUVPBMI3C7p6d6inXdGjwYSwOF7qb8Ce4Z641PpYVyOm5K6tM08BhAoGBALNkfH3ZPImRKN19bGcHAtrzoDMFAAQrhjyMr9aUpMGLoAjxh1U2pYbj1y0e03ArUT/TRzOXNbHuST82tDZEarVSF8RmU8DNUAHSI9dmtXPtgeKpG8TNyP5q3VnJEHdSRZVLb5pGfZuo3tegsHwZF/oCGK+GHD/fhmSFB04GPJon
|
||||
notifyUrl: https://peaktcm.com/applet/huifuPay/callback
|
||||
productId: EDUSTD
|
||||
configureAppletCallbackUrl: https://peaktcm.com/applet/huifuPay/configureAppletCallback
|
||||
|
||||
huifu-vendor:
|
||||
sysId: 6666000188069628
|
||||
huifuId: 6666000188735389
|
||||
publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0uJDRMFj1xoQCJbReVaNdiqjL9vdKolMzWKe2vMhZwkBlgQMpaqtHqNKNCn3DPL/7ugPUNzj4PLr3PSR4DK+TBOOa3xRuKLt3s2x6wouVNpiRRglUzh5OgciXXYPPl7QGu2YlJZ9F7ikiaD7KG9PJI7oidbA7lQ4qMcIGlSBUF4l3TeoOpad0zLs41IRJJPIMKPf5Jj9HS/1Tggcn7f36h1l4JWpTRyfB8HyF66IHzA3mQa5yskUHf1LLuO0hxtIfy9Ua+xJYqEWD334xaMzauZQsLM98d1Pj7I1YRvzNohabpCJqO1wF6PDB3rqdLarAZagN/JhYIHHGXMBGfQZxwIDAQAB
|
||||
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDS4kNEwWPXGhAIltF5Vo12KqMv290qiUzNYp7a8yFnCQGWBAylqq0eo0o0KfcM8v/u6A9Q3OPg8uvc9JHgMr5ME45rfFG4ou3ezbHrCi5U2mJFGCVTOHk6ByJddg8+XtAa7ZiUln0XuKSJoPsob08kjuiJ1sDuVDioxwgaVIFQXiXdN6g6lp3TMuzjUhEkk8gwo9/kmP0dL/VOCByft/fqHWXglalNHJ8HwfIXrogfMDeZBrnKyRQd/Usu47SHG0h/L1Rr7ElioRYPffjFozNq5lCwsz3x3U+PsjVhG/M2iFpukImo7XAXo8MHeup0tqsBlqA38mFggccZcwEZ9BnHAgMBAAECggEABRZaQxVbQDwruNlSklfJL9mVeDkYm8cWKDpB1MxauO6ZsDHG0FROutd7mciNYi2WiP/mJrA9exbBI+JuhncS6ijqnaMxIYAktuRFb4MeXQOrrTkI5XoOSp0Tcie/9GQ7JvsmXCdJO0JEDeHpTS9qPFqKN5ELtHQprcCnbGMvf23KyqOFx71Sbe4JxDqV4j0MeJJ/ZEOnLHJAhJ/o/0Gw+2cAVBxOq/xDTiSzKQaR4Cy7h3HMlDXnBXlAwKLb7x/F6efFyBv3RGZjtTgt8G7319icRJUHyXa3JPk/UbKTHrZ3saCj0ByAPpCwSVx6uNxhVeEOFqi3VwtV8s4ClAJtUQKBgQDwEGA1Shr2klTnM1VN+R3YkgaG8QtkZQQG77vCR1q3uMPKpWUQOi/2wQs3SbTAr7ZX+8CHNb8M3KGVAEFR2PDEFok/LA0snFTwdL6dLkXkmOxmNyR2wmXIUimMBogQIc4bI11EpPoocbrzuJ3q48fDIAr7Hc4d/0HasEAQHvAdfwKBgQDg4gCr5M7atD4XdKqmNs5kltjw36EeSig2cajfQb7aARmHS+1F9cn1cpgTvqMBHxBZBx5cW1ETLTxDEq8/ONHHAtmgCVgrc0lgch+Dph1Nt4Jl/aNxxaZjLpLQtcHnFR4/7RGuJMUTAzAUcOer9XiCJskE2EVLrT53pE665Se3uQKBgFcXmc+7cszBicvW87u2BP3s/DE4gcmKXEun7ZYuDNtaTK3YEzIv6fa7UjZcpxbAqIaC7vYeqZEg+f30eb4X1kFNa71fXegxTx9tOzoK75BfamQq3S+4lvR12lzSgcpYeU2fJtcggyDpIcoJ0EDHIlHH7jPsPDrlwB52ksdGchZBAoGBAM49dvAuf6cD9o38aSTWzqJh1cPS1Ci7SfXkd0OJQEcfCxKSB6FJsVZxKhexljzJCG0dojKBbPFxYvsAf8gzi/BAJOYoq7vtlzXkdBKAng9QZT60Rtlosxfy9qKCjronG/70pGpZPoQZQFb+1PlXvpVejKzsy2CEGe4PukSdNLfBAoGATUJ7FGVmG2Ym8UHcP0bMeWwS7XROuiquliLtamWwpZTByZaLO1nK2FtC1oVj3il4Wox6/In1bBbdtqxmRmKcD9q1JExr1PHxXQTUbIoWb4GXNHq7h5kOhpra1uVRmNIewfJUYTCEzzHTrJe8dADBc7lGpsibwk174YD2343Aupw=
|
||||
productId: PAYUN
|
||||
notifyUrl: https://peaktcm.com/hf/webhook/vendorPaySuccess
|
||||
refundApi: https://api.huifu.com/v3/trade/payment/scanpay/refund
|
||||
|
||||
huaweiyun:
|
||||
accessKey: HPUACMRQ2NKQNOXNCFNO
|
||||
secretKey: KC3yEw7ARoL0oxcmXy7vE3hyMBPKjLsG1LCSQdYh
|
||||
@@ -27,8 +27,8 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 开发环境配置 -->
|
||||
<springProfile name="dev">
|
||||
<!-- 开发与本地环境配置 -->
|
||||
<springProfile name="dev,local">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
|
||||
@@ -106,6 +106,9 @@ public class Device {
|
||||
|
||||
private Integer treatStatus;
|
||||
|
||||
@ExcelProperty(value = "设备标签")
|
||||
private String deviceTags;
|
||||
|
||||
|
||||
@ExcelProperty(value = "机型品类")
|
||||
private String modelCategory;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package cn.unizk.entity.po;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备治疗参数定制规则实体
|
||||
*
|
||||
* @author hao / antigravity
|
||||
* @date 2026/10/19
|
||||
*/
|
||||
@Data
|
||||
@TableName("device_treat_rule")
|
||||
public class DeviceTreatRule implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 适用对象类型:1-单机(deviceId), 2-设备标签(tag), 3-机型分类(model/cf)
|
||||
*/
|
||||
private Integer targetType;
|
||||
|
||||
/**
|
||||
* 匹配标识值:如具体的 deviceId / 标签名(golf_vip) / 机型配置(1/2)
|
||||
*/
|
||||
private String targetValue;
|
||||
|
||||
/**
|
||||
* 关联的基准病症ID (对应 treat_parameter.id)
|
||||
*/
|
||||
private Integer symptomId;
|
||||
|
||||
/**
|
||||
* 关联病症名称(非数据库字段,用于前端展示)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String symptomsName;
|
||||
|
||||
/**
|
||||
* 定制频率(01-99)
|
||||
*/
|
||||
private String fr;
|
||||
|
||||
/**
|
||||
* 定制脉宽
|
||||
*/
|
||||
private String pw;
|
||||
|
||||
/**
|
||||
* 定制脉幅
|
||||
*/
|
||||
private String pl;
|
||||
|
||||
/**
|
||||
* 定制超声幅度
|
||||
*/
|
||||
private String ul;
|
||||
|
||||
/**
|
||||
* 定制理疗时长
|
||||
*/
|
||||
private String tl;
|
||||
|
||||
/**
|
||||
* 优先级权重 (数值越大越优先生效)
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 状态:1-启用, 0-禁用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 规则备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.unizk.entity.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备标签批量设置 VO
|
||||
*
|
||||
* @author hao / antigravity
|
||||
* @date 2026/10/19
|
||||
*/
|
||||
@Data
|
||||
public class DeviceTagsVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 目标设备ID列表
|
||||
*/
|
||||
private List<String> deviceIds;
|
||||
|
||||
/**
|
||||
* 标签列表(英文逗号分隔,如 "golf_vip,elderly_care")
|
||||
*/
|
||||
private String tags;
|
||||
|
||||
/**
|
||||
* 机型品类编码(可选)
|
||||
*/
|
||||
private String modelCategory;
|
||||
|
||||
/**
|
||||
* 设置模式:1-覆盖(默认),2-追加
|
||||
*/
|
||||
private Integer mode = 1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.unizk.mapper;
|
||||
|
||||
import cn.unizk.entity.po.DeviceTreatRule;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 设备治疗参数定制规则 Mapper 接口
|
||||
*
|
||||
* @author hao / antigravity
|
||||
* @date 2026/10/19
|
||||
*/
|
||||
@Mapper
|
||||
public interface DeviceTreatRuleMapper extends BaseMapper<DeviceTreatRule> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
UniAI Health (原愈极刻) 定制设备治疗参数功能 - 端到端全链路验证工具
|
||||
1. 校验 RESTful API (管理员登录鉴权、规则管理、标签打标)
|
||||
2. 模拟 IoT 设备与后台 UDP 服务 (端口 61100) 双向报文交互
|
||||
3. 验证四层参数决策引擎:标签定制、单机定制、安全截断兜底、基准兜底
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
GREEN = "\033[92m"
|
||||
RED = "\033[91m"
|
||||
YELLOW = "\033[93m"
|
||||
BLUE = "\033[94m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
UDP_HOST = "127.0.0.1"
|
||||
UDP_PORT = 61100
|
||||
HTTP_BASE = "http://127.0.0.1:8082"
|
||||
|
||||
def calc_crc(content: str) -> str:
|
||||
"""计算异或校验码 (XOR Checksum)"""
|
||||
if "$" in content and "*" in content:
|
||||
raw = content[content.index("$") + 1 : content.rindex("*")]
|
||||
else:
|
||||
raw = content
|
||||
xor_val = 0
|
||||
for ch in raw.encode("ascii"):
|
||||
xor_val ^= ch
|
||||
return f"{xor_val:02X}"
|
||||
|
||||
def build_packet(inner_body: str) -> bytes:
|
||||
"""构建带 $ 与 *{CRC}\\r\\n 的完整报文"""
|
||||
raw = f"${inner_body}*"
|
||||
crc = calc_crc(raw)
|
||||
packet_str = f"${inner_body}*{crc}\r\n"
|
||||
return packet_str.encode("utf-8")
|
||||
|
||||
def parse_kv(packet_str: str) -> dict:
|
||||
"""解析以管道符分隔的报文内容"""
|
||||
packet_str = packet_str.strip().strip("\r\n")
|
||||
if packet_str.startswith("$"):
|
||||
packet_str = packet_str[1:]
|
||||
if "*" in packet_str:
|
||||
packet_str = packet_str[:packet_str.rindex("*")]
|
||||
|
||||
parts = packet_str.split("|")
|
||||
res = {}
|
||||
if parts:
|
||||
res["CMD"] = parts[0]
|
||||
for p in parts[1:]:
|
||||
if len(p) >= 2:
|
||||
res[p[:2]] = p[2:]
|
||||
return res
|
||||
|
||||
def test_login() -> str:
|
||||
"""登录系统获取管理员 JWT Token"""
|
||||
url = f"{HTTP_BASE}/pc/loginByUsername"
|
||||
payload = json.dumps({"username": "admin", "password": "admin123"}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=3) as resp:
|
||||
raw_data = json.loads(resp.read().decode("utf-8"))
|
||||
token = raw_data.get("token")
|
||||
if not token and isinstance(raw_data.get("data"), dict):
|
||||
token = raw_data["data"].get("token")
|
||||
if token:
|
||||
print(f" {GREEN}✓ 管理员身份认证成功 (username: admin){RESET}")
|
||||
return token
|
||||
else:
|
||||
print(f" {RED}✗ 登录成功但未包含 Token: {raw_data}{RESET}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f" {RED}✗ 管理员登录异常: {e}{RESET}")
|
||||
return ""
|
||||
|
||||
def test_rest_api(token: str):
|
||||
"""测试 REST 接口:规则分页与批量标签"""
|
||||
print(f"\n{BLUE}▶ 测试用例: [REST API] 验证定制规则管理与标签接口{RESET}")
|
||||
if not token:
|
||||
print(f" {RED}✗ 缺少认证 Token,跳过鉴权接口测试{RESET}")
|
||||
return False
|
||||
|
||||
headers = {
|
||||
"jkzy-auth": token,
|
||||
"User-Agent": "VerifyScript",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 1. 查询规则列表
|
||||
url_rules = f"{HTTP_BASE}/pc/admin/deviceTreatRule/page?pageNo=1&pageSize=10"
|
||||
try:
|
||||
req = urllib.request.Request(url_rules, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=3) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
if data.get("code") == 200:
|
||||
records = data.get("data", {}).get("records", [])
|
||||
print(f" {GREEN}✓ GET /pc/admin/deviceTreatRule/page 响应成功,已加载 {len(records)} 条定制规则{RESET}")
|
||||
for r in records[:3]:
|
||||
print(f" - [{r.get('ruleName')}] 类型:{r.get('targetType')} 目标:{r.get('targetValue')} FR:{r.get('fr')} UL:{r.get('ul')}")
|
||||
else:
|
||||
print(f" {RED}✗ 规则接口返回错误: {data}{RESET}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" {RED}✗ 规则列表接口访问失败: {e}{RESET}")
|
||||
return False
|
||||
|
||||
# 2. 批量打标测试
|
||||
url_tags = f"{HTTP_BASE}/pc/admin/device/setTags"
|
||||
try:
|
||||
payload = json.dumps({
|
||||
"deviceIds": ["DEV_DEF_04"],
|
||||
"tags": "temp_test_tag,wsl_verify"
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url_tags, data=payload, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=3) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
if data.get("code") == 200:
|
||||
print(f" {GREEN}✓ POST /pc/admin/device/setTags 批量设备标签更新成功{RESET}")
|
||||
else:
|
||||
print(f" {YELLOW}⚠ 打标接口响应: {data.get('message')}{RESET}")
|
||||
except Exception as e:
|
||||
print(f" {YELLOW}⚠ 打标接口调用异常: {e}{RESET}")
|
||||
|
||||
return True
|
||||
|
||||
def test_device_treatment_params(device_id: str, case_name: str, expected_params: dict):
|
||||
"""通过 UDP 模拟设备请求并断言下发的参数"""
|
||||
print(f"\n{BLUE}▶ 测试用例: [{device_id}] - {case_name}{RESET}")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(3.0)
|
||||
|
||||
try:
|
||||
# 第一步:设备上报状态 03(就绪等待处方)
|
||||
req1 = build_packet(f"03|MU{device_id}|CI00|CN00|")
|
||||
# print(f" [TX -> 61100] {req1.decode().strip()}")
|
||||
sock.sendto(req1, (UDP_HOST, UDP_PORT))
|
||||
|
||||
# 接收服务端的 03 回复(包含生成的处方流水号 CI)
|
||||
data1, _ = sock.recvfrom(2048)
|
||||
resp1_str = data1.decode("utf-8", errors="ignore")
|
||||
# print(f" [RX <- 61100] {resp1_str.strip()}")
|
||||
kv1 = parse_kv(resp1_str)
|
||||
|
||||
ci = kv1.get("CI")
|
||||
if not ci or ci == "00":
|
||||
print(f" {RED}✗ 失败: 服务端未返回有效处方流水号 (CI={ci}){RESET}")
|
||||
return False
|
||||
|
||||
# 第二步:设备使用状态 30 请求处方参数分段 TN1
|
||||
req2 = build_packet(f"30|MU{device_id}|CI{ci}|TN1|")
|
||||
# print(f" [TX -> 61100] {req2.decode().strip()}")
|
||||
sock.sendto(req2, (UDP_HOST, UDP_PORT))
|
||||
|
||||
# 接收服务端下发的 30 处方参数报文
|
||||
data2, _ = sock.recvfrom(2048)
|
||||
resp2_str = data2.decode("utf-8", errors="ignore")
|
||||
# print(f" [RX <- 61100] {resp2_str.strip()}")
|
||||
kv2 = parse_kv(resp2_str)
|
||||
|
||||
# 断言校验
|
||||
all_passed = True
|
||||
actual_details = []
|
||||
for param, exp_val in expected_params.items():
|
||||
act_val = kv2.get(param)
|
||||
# 兼容前导 0
|
||||
match = (act_val == exp_val) or (act_val and act_val.lstrip("0") == exp_val.lstrip("0"))
|
||||
if match:
|
||||
actual_details.append(f"{param}={act_val} {GREEN}✓{RESET}")
|
||||
else:
|
||||
actual_details.append(f"{param}={act_val}(预期:{exp_val}) {RED}✗{RESET}")
|
||||
all_passed = False
|
||||
|
||||
print(" 参数下发结果: " + ", ".join(actual_details))
|
||||
if all_passed:
|
||||
print(f" {GREEN}✓ PASS: 该设备下发参数完全符合定制/安全/基准规则预期!{RESET}")
|
||||
return True
|
||||
else:
|
||||
print(f" {RED}✗ FAIL: 参数与预期不符!{RESET}")
|
||||
return False
|
||||
|
||||
except socket.timeout:
|
||||
print(f" {RED}✗ 接收超时: 服务端未及时回复 UDP 报文{RESET}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" {RED}✗ 异常: {e}{RESET}")
|
||||
return False
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def main():
|
||||
print(f"{BLUE}=================================================================={RESET}")
|
||||
print(f"{BLUE} UniAI Health 定制治疗参数功能 WSL 本地全链路验证工具 {RESET}")
|
||||
print(f"{BLUE}=================================================================={RESET}")
|
||||
|
||||
# 检查 HTTP 服务就绪
|
||||
print(f"\n正在检查后台 HTTP (8082) 与 UDP (61100) 端口可用性...")
|
||||
http_ready = False
|
||||
try:
|
||||
with urllib.request.urlopen(f"{HTTP_BASE}/pc/loginByUsername", timeout=2) as resp:
|
||||
http_ready = True
|
||||
except urllib.error.HTTPError as e:
|
||||
# POST endpoint called with GET will return 405 Method Not Allowed, which proves server is up
|
||||
http_ready = (e.code in (400, 405))
|
||||
except Exception:
|
||||
http_ready = False
|
||||
|
||||
if not http_ready:
|
||||
print(f"{RED}✗ 后台服务尚未就绪或 8082 端口未启动。{RESET}")
|
||||
print(f"{YELLOW}请先在 WSL-Ubuntu 终端中运行以下命令启动服务:{RESET}")
|
||||
print(f" cd /mnt/d/Workspace/uniaihealth/server")
|
||||
print(f" mvn spring-boot:run -pl admin -Dspring-boot.run.profiles=local\n")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{GREEN}✓ 后台服务连接成功!开始执行测试用例...{RESET}")
|
||||
|
||||
# 登录获取 Token
|
||||
token = test_login()
|
||||
|
||||
results = []
|
||||
|
||||
# 1. REST API
|
||||
results.append(("REST API 测试", test_rest_api(token)))
|
||||
|
||||
# 2. 标签匹配覆盖 (DEV_GOLF_01 打了 golf_vip 标签)
|
||||
results.append(("标签定制覆盖 (golf_vip)", test_device_treatment_params(
|
||||
device_id="DEV_GOLF_01",
|
||||
case_name="标签定制匹配 (golf_vip 覆盖基准)",
|
||||
expected_params={"FR": "08", "PW": "55", "PL": "12", "UL": "05", "TL": "025"}
|
||||
)))
|
||||
|
||||
# 3. 单机精确匹配覆盖 (DEV_SPEC_02 配置了单机规则)
|
||||
results.append(("单机专属定制 (DEV_SPEC_02)", test_device_treatment_params(
|
||||
device_id="DEV_SPEC_02",
|
||||
case_name="单机专属规则覆盖 (DEV_SPEC_02 覆盖基准与标签)",
|
||||
expected_params={"FR": "02", "PW": "40", "PL": "04", "UL": "02", "TL": "010"}
|
||||
)))
|
||||
|
||||
# 4. 非医疗民用版安全极限截断 (DEV_CIVIL_03, cf=2, 规则配置超标 UL=18)
|
||||
results.append(("民用机安全截断 (DEV_CIVIL_03)", test_device_treatment_params(
|
||||
device_id="DEV_CIVIL_03",
|
||||
case_name="硬件安全底线防护 (cf=2 非医疗版强制限幅 UL <= 10)",
|
||||
expected_params={"UL": "10"}
|
||||
)))
|
||||
|
||||
# 5. 普通设备基准默认参数兜底 (DEV_DEF_04, 无标签无单机规则)
|
||||
results.append(("基准参数兜底 (DEV_DEF_04)", test_device_treatment_params(
|
||||
device_id="DEV_DEF_04",
|
||||
case_name="全局病症基准参数正常下发兜底",
|
||||
expected_params={"FR": "03", "PW": "50", "PL": "08", "UL": "01", "TL": "017"}
|
||||
)))
|
||||
|
||||
# 汇总
|
||||
print(f"\n{BLUE}=================================================================={RESET}")
|
||||
print(f"{BLUE} 测试结果汇总报告 {RESET}")
|
||||
print(f"{BLUE}=================================================================={RESET}")
|
||||
total = len(results)
|
||||
passed = sum(1 for _, ok in results if ok)
|
||||
for name, ok in results:
|
||||
status_text = f"{GREEN}PASS ✓{RESET}" if ok else f"{RED}FAIL ✗{RESET}"
|
||||
print(f" {name: <35} : {status_text}")
|
||||
print(f"{BLUE}------------------------------------------------------------------{RESET}")
|
||||
print(f"总计: {total} 项,成功: {GREEN}{passed}{RESET} 项,失败: {RED}{total - passed}{RESET} 项")
|
||||
|
||||
if passed == total:
|
||||
print(f"{GREEN}★ 所有定制治疗参数与调度验证均 100% 成功通过!★{RESET}\n")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"{RED}⚠ 存在未通过的测试项,请检查日志。{RESET}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,6 +18,7 @@ const files = {
|
||||
export: 'exportDevices',
|
||||
remark: 'remark',
|
||||
frequency: 'getTopThree',
|
||||
setTags: 'setTags',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 设备治疗参数定制规则 API
|
||||
*/
|
||||
export const getRulePage = params => {
|
||||
return request({
|
||||
url: '/api/pc/admin/deviceTreatRule/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const saveOrUpdateRule = data => {
|
||||
return request({
|
||||
url: '/api/pc/admin/deviceTreatRule/saveOrUpdate',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteRule = id => {
|
||||
return request({
|
||||
url: '/api/pc/admin/deviceTreatRule/delete',
|
||||
method: 'post',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
|
||||
export const changeRuleStatus = (id, status) => {
|
||||
return request({
|
||||
url: '/api/pc/admin/deviceTreatRule/changeStatus',
|
||||
method: 'post',
|
||||
params: { id, status }
|
||||
})
|
||||
}
|
||||
|
||||
export const getRuleDetail = id => {
|
||||
return request({
|
||||
url: '/api/pc/admin/deviceTreatRule/getOne',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="title" width="700px" align-center draggable @closed="resetFields">
|
||||
<section class="p-4">
|
||||
<el-form :model="formData" label-width="110px">
|
||||
<el-form-item label="操作模式:">
|
||||
<el-radio-group v-model="formData.mode">
|
||||
<el-radio :label="1">覆盖原有标签</el-radio>
|
||||
<el-radio :label="2">追加新标签</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="快捷标签:">
|
||||
<div class="tag-shortcuts">
|
||||
<el-check-tag
|
||||
v-for="tag in commonTags"
|
||||
:key="tag.value"
|
||||
:checked="selectedTags.includes(tag.value)"
|
||||
@change="handleTagToggle(tag.value)"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</el-check-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="设备标签:">
|
||||
<el-input
|
||||
v-model.trim="formData.tags"
|
||||
placeholder="支持输入多个标签,以英文逗号分隔,如:golf_vip,elderly_care"
|
||||
clearable
|
||||
/>
|
||||
<div class="tip-text">用于匹配参数定制规则,例如打上 "golf_vip" 即可享受高尔夫专属治疗参数</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="机型品类:">
|
||||
<el-select v-model="formData.modelCategory" clearable placeholder="可选:指定机型品类编码" style="width: 100%;">
|
||||
<el-option label="标准款 (DEFAULT)" value="DEFAULT" />
|
||||
<el-option label="尊享款 (PRO)" value="PRO" />
|
||||
<el-option label="体验款 (TRIAL)" value="TRIAL" />
|
||||
<el-option label="轻便款 (LITE)" value="LITE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选中设备数:">
|
||||
<span style="font-weight: bold; color: #409eff;">共 {{ deviceList.length }} 台设备</span>
|
||||
<div class="device-chips mt-1">
|
||||
<el-tag
|
||||
v-for="item in deviceList"
|
||||
:key="item.deviceId"
|
||||
size="small"
|
||||
class="mr-1 mb-1"
|
||||
closable
|
||||
@close="handleRemoveDevice(item.deviceId)"
|
||||
>
|
||||
{{ item.deviceId }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleConfirm">确认保存</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, getCurrentInstance, watch } from 'vue'
|
||||
import { api } from '@/apis/admin/device'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const emit = defineEmits(['success'])
|
||||
const { title = '设置设备标签' } = defineProps({
|
||||
title: String
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const deviceList = ref([])
|
||||
const selectedTags = ref([])
|
||||
|
||||
const commonTags = [
|
||||
{ label: '高尔夫专享 (golf_vip)', value: 'golf_vip' },
|
||||
{ label: '养老康养示范 (elderly_care)', value: 'elderly_care' },
|
||||
{ label: '运动康复专科 (sports_rehab)', value: 'sports_rehab' },
|
||||
{ label: 'VIP特护定制 (vip_care)', value: 'vip_care' },
|
||||
{ label: '体验展厅机 (showroom)', value: 'showroom' }
|
||||
]
|
||||
|
||||
const formData = reactive({
|
||||
tags: '',
|
||||
mode: 1,
|
||||
modelCategory: ''
|
||||
})
|
||||
|
||||
const handleTagToggle = tagValue => {
|
||||
const index = selectedTags.value.indexOf(tagValue)
|
||||
if (index > -1) {
|
||||
selectedTags.value.splice(index, 1)
|
||||
} else {
|
||||
selectedTags.value.push(tagValue)
|
||||
}
|
||||
formData.tags = selectedTags.value.join(',')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.tags,
|
||||
val => {
|
||||
if (!val) {
|
||||
selectedTags.value = []
|
||||
return
|
||||
}
|
||||
const current = val.split(',').map(s => s.trim()).filter(Boolean)
|
||||
selectedTags.value = current
|
||||
}
|
||||
)
|
||||
|
||||
const resetFields = () => {
|
||||
formData.tags = ''
|
||||
formData.mode = 1
|
||||
formData.modelCategory = ''
|
||||
deviceList.value = []
|
||||
selectedTags.value = []
|
||||
}
|
||||
|
||||
const handleRemoveDevice = id => {
|
||||
const idx = deviceList.value.findIndex(item => item.deviceId === id)
|
||||
if (idx > -1) {
|
||||
deviceList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!deviceList.value.length) {
|
||||
return proxy.$message.warning('请至少保留一台设备')
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await api({
|
||||
type: 'setTags',
|
||||
data: {
|
||||
deviceIds: deviceList.value.map(item => item.deviceId),
|
||||
tags: formData.tags,
|
||||
modelCategory: formData.modelCategory,
|
||||
mode: formData.mode
|
||||
}
|
||||
})
|
||||
const { code, message } = res
|
||||
if (code !== 200 && code !== undefined && res !== true) {
|
||||
return proxy.$message.error(message || '保存失败')
|
||||
}
|
||||
proxy.$message.success('设备标签设置成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const show = list => {
|
||||
dialogVisible.value = true
|
||||
deviceList.value = [...list]
|
||||
if (list.length === 1 && list[0].deviceTags) {
|
||||
formData.tags = list[0].deviceTags
|
||||
formData.modelCategory = list[0].modelCategory || ''
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.device-chips {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
background-color: #f8f9fa;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tag-shortcuts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="title" width="680px" align-center draggable @closed="handleClosed">
|
||||
<section class="p-4">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" label-position="right">
|
||||
<el-form-item label="规则名称" prop="ruleName">
|
||||
<el-input v-model.trim="formData.ruleName" placeholder="例如:高尔夫专享温和理疗" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="适用对象类型" prop="targetType">
|
||||
<el-radio-group v-model="formData.targetType" @change="handleTypeChange">
|
||||
<el-radio :label="2">设备标签 (Tag)</el-radio>
|
||||
<el-radio :label="3">机型/配置 (Category)</el-radio>
|
||||
<el-radio :label="1">指定单机 (Device ID)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="匹配目标标识" prop="targetValue">
|
||||
<!-- 标签类型推荐 -->
|
||||
<div v-if="formData.targetType === 2" style="width: 100%;">
|
||||
<el-select
|
||||
v-model="formData.targetValue"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请选择或输入标签名,如 golf_vip"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option label="高尔夫专享 (golf_vip)" value="golf_vip" />
|
||||
<el-option label="养老康养示范 (elderly_care)" value="elderly_care" />
|
||||
<el-option label="运动康复专科 (sports_rehab)" value="sports_rehab" />
|
||||
<el-option label="VIP特护定制 (vip_care)" value="vip_care" />
|
||||
<el-option label="体验展厅机 (showroom)" value="showroom" />
|
||||
</el-select>
|
||||
<div class="tip-text">设备只要打上该标签即可生效</div>
|
||||
</div>
|
||||
|
||||
<!-- 机型配置类型 -->
|
||||
<div v-else-if="formData.targetType === 3" style="width: 100%;">
|
||||
<el-select v-model="formData.targetValue" placeholder="请选择机器配置版本" style="width: 100%;">
|
||||
<el-option label="医疗版本 (1)" value="1" />
|
||||
<el-option label="非医疗版本 (2)" value="2" />
|
||||
</el-select>
|
||||
<div class="tip-text">所有属于该配置版本的设备均会生效</div>
|
||||
</div>
|
||||
|
||||
<!-- 单机类型 -->
|
||||
<div v-else style="width: 100%;">
|
||||
<el-input v-model.trim="formData.targetValue" placeholder="请输入具体设备ID,如 12345678" />
|
||||
<div class="tip-text">单机规则具有最高生效优先级</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="关联病症" prop="symptomId">
|
||||
<el-select
|
||||
v-model="formData.symptomId"
|
||||
filterable
|
||||
placeholder="请选择要覆盖的基准病症"
|
||||
style="width: 100%;"
|
||||
:loading="symptomLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in symptomList"
|
||||
:key="item.id"
|
||||
:label="`${item.symptomsName} (${item.acupoint || '常规'}) - 基准参数: 超声${item.ul} / 频率${item.fr}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">定制治疗参数(留空则自动继承基准参数)</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="超声幅度 (ul)">
|
||||
<el-input v-model.trim="formData.ul" placeholder="留空继承默认" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉冲幅度 (pl)">
|
||||
<el-input v-model.trim="formData.pl" placeholder="留空继承默认" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉冲宽度 (pw)">
|
||||
<el-input v-model.trim="formData.pw" placeholder="留空继承默认" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉冲频率 (fr)">
|
||||
<el-input v-model.trim="formData.fr" placeholder="留空继承默认" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="理疗时长 (tl)">
|
||||
<el-input v-model.trim="formData.tl" placeholder="留空继承默认,如 015 表示15分钟" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="优先级权重" prop="priority">
|
||||
<el-input-number v-model="formData.priority" :min="1" :max="999" />
|
||||
<div class="tip-text">同级别冲突时数值越大越优先</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启停状态">
|
||||
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="规则备注">
|
||||
<el-input v-model.trim="formData.remark" type="textarea" :rows="2" placeholder="填写业务背景或备注说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存规则</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, getCurrentInstance } from 'vue'
|
||||
import { saveOrUpdateRule } from '@/apis/admin/deviceTreatRule'
|
||||
import { api as therapistParamsApi } from '@/apis/admin/therapistParams'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const emit = defineEmits(['success'])
|
||||
const { title = '定制治疗参数规则' } = defineProps({
|
||||
title: String
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const symptomLoading = ref(false)
|
||||
const symptomList = ref([])
|
||||
const formRef = ref()
|
||||
|
||||
const formData = reactive({
|
||||
id: undefined,
|
||||
ruleName: '',
|
||||
targetType: 2,
|
||||
targetValue: '',
|
||||
symptomId: undefined,
|
||||
fr: '',
|
||||
pw: '',
|
||||
pl: '',
|
||||
ul: '',
|
||||
tl: '',
|
||||
priority: 10,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
targetType: [{ required: true, message: '请选择适用对象类型', trigger: 'change' }],
|
||||
targetValue: [{ required: true, message: '请输入或选择匹配标识', trigger: 'change' }],
|
||||
symptomId: [{ required: true, message: '请选择关联病症', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleTypeChange = () => {
|
||||
formData.targetValue = ''
|
||||
}
|
||||
|
||||
const loadSymptoms = async () => {
|
||||
if (symptomList.value.length) return
|
||||
symptomLoading.value = true
|
||||
try {
|
||||
const res = await therapistParamsApi({
|
||||
type: 'list',
|
||||
method: 'get'
|
||||
})
|
||||
const { code, data } = res
|
||||
if (code === 200 || Array.isArray(data)) {
|
||||
symptomList.value = data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
symptomLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClosed = () => {
|
||||
formData.id = undefined
|
||||
formData.ruleName = ''
|
||||
formData.targetType = 2
|
||||
formData.targetValue = ''
|
||||
formData.symptomId = undefined
|
||||
formData.fr = ''
|
||||
formData.pw = ''
|
||||
formData.pl = ''
|
||||
formData.ul = ''
|
||||
formData.tl = ''
|
||||
formData.priority = 10
|
||||
formData.status = 1
|
||||
formData.remark = ''
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const show = async (data = null) => {
|
||||
dialogVisible.value = true
|
||||
await loadSymptoms()
|
||||
if (data) {
|
||||
formData.id = data.id
|
||||
formData.ruleName = data.ruleName
|
||||
formData.targetType = data.targetType
|
||||
formData.targetValue = data.targetValue
|
||||
formData.symptomId = data.symptomId
|
||||
formData.fr = data.fr || ''
|
||||
formData.pw = data.pw || ''
|
||||
formData.pl = data.pl || ''
|
||||
formData.ul = data.ul || ''
|
||||
formData.tl = data.tl || ''
|
||||
formData.priority = data.priority || 10
|
||||
formData.status = data.status !== undefined ? data.status : 1
|
||||
formData.remark = data.remark || ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async valid => {
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await saveOrUpdateRule({ ...formData })
|
||||
const { code, message } = res
|
||||
if (code !== 200 && code !== undefined && res !== true) {
|
||||
return proxy.$message.error(message || '保存失败')
|
||||
}
|
||||
proxy.$message.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -17,6 +17,10 @@ export default [
|
||||
path: '/admin/physio/therapistParams',
|
||||
component: () => import('@/views/admin/physio/therapistParams.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin/physio/customParams',
|
||||
component: () => import('@/views/admin/physio/customParams.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin/physio/reportsTemp',
|
||||
component: () => import('@/views/admin/physio/reportsTemp.vue')
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
<el-tooltip :disabled="isMultiple" effect="dark" content="本页至少选择一项" placement="bottom">
|
||||
<el-button :disabled="!isMultiple" type="primary" @click="handleSetDeviceConfig">设备配置</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip :disabled="isMultiple" effect="dark" content="本页至少选择一项" placement="bottom">
|
||||
<el-button :disabled="!isMultiple" type="primary" @click="handleSetDeviceTags">设置标签</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip :disabled="isMultiple" effect="dark" content="本页至少选择一项" placement="bottom">
|
||||
<el-button :disabled="!isMultiple" type="primary" @click="handleDeviceClear">设备清零</el-button>
|
||||
</el-tooltip>
|
||||
@@ -132,6 +135,23 @@
|
||||
|
||||
<el-table-column label="机器配置" align="center" prop="cfWord" width="200" />
|
||||
|
||||
<el-table-column label="设备标签" align="center" prop="deviceTags" width="180">
|
||||
<template #default="props">
|
||||
<div v-if="props.row.deviceTags" class="flex flex-wrap justify-center">
|
||||
<el-tag
|
||||
v-for="tag in props.row.deviceTags.split(',').filter(Boolean)"
|
||||
:key="tag"
|
||||
size="small"
|
||||
class="mr-1 mb-1"
|
||||
type="info"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="工作模式" align="center" prop="wmWord" width="200" />
|
||||
|
||||
<el-table-column label="硬件版本" align="center" prop="hw" width="200" />
|
||||
@@ -171,6 +191,7 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleShowDeviceLocation(props.row)">位置</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleSetSingleDeviceTags(props.row)">设置标签</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleCheckUseRecord(props.row.deviceId)">使用记录</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleCheckRechargeRecord(props.row.deviceId)">充值记录</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleSetRemark(props.row)">备注</el-dropdown-item>
|
||||
@@ -206,11 +227,15 @@
|
||||
<!-- 设备配置 -->
|
||||
<set-device-config ref="setDeviceConfigRef" title="设备配置" @success="handleInsideSuccess" />
|
||||
|
||||
<!-- 设置标签 -->
|
||||
<set-device-tags ref="setDeviceTagsRef" @success="handleInsideSuccess" />
|
||||
|
||||
<!-- 备注 -->
|
||||
<remark-form ref="remarkFormRef" title="修改备注" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SetDeviceTags from '@/components/admin/device/SetDeviceTags.vue'
|
||||
import RemarkForm from '@/components/admin/device/RemarkForm.vue'
|
||||
import RechargeRecord from '@/components/admin/device/RechargeRecord.vue'
|
||||
import SetDeviceConfig from '@/components/admin/device/SetDeviceConfig.vue'
|
||||
@@ -449,6 +474,14 @@ const handleSetDeviceConfig = () => {
|
||||
setDeviceConfigRef.value.show([...multipleSelection.value])
|
||||
}
|
||||
|
||||
const setDeviceTagsRef = ref()
|
||||
const handleSetDeviceTags = () => {
|
||||
setDeviceTagsRef.value.show([...multipleSelection.value])
|
||||
}
|
||||
const handleSetSingleDeviceTags = row => {
|
||||
setDeviceTagsRef.value.show([row])
|
||||
}
|
||||
|
||||
const handleDeviceClear = () => {
|
||||
proxy.$messageBox.confirm('是否确认清空使用次数?', '提示', {
|
||||
confirmButtonText: '确认',
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<section class="full-height flex flex-column">
|
||||
<header class="header p-2">
|
||||
<el-form inline class="pl-2 pt-2">
|
||||
<el-form-item label="规则名称:">
|
||||
<el-input v-model.trim="queryForm.ruleName" clearable placeholder="输入规则名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="适用类型:">
|
||||
<el-select v-model="queryForm.targetType" clearable placeholder="全部" style="width: 150px;">
|
||||
<el-option label="单台设备" :value="1" />
|
||||
<el-option label="设备标签" :value="2" />
|
||||
<el-option label="机型分类" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态:">
|
||||
<el-select v-model="queryForm.status" clearable placeholder="全部" style="width: 120px;">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button type="success" @click="handleAddClick">添加定制规则</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</header>
|
||||
|
||||
<main class="main hidden flex-1 pl-2 pr-2 pt-2 pb-2 mt-2">
|
||||
<el-table :data="tableData.list" v-loading="loading" row-key="id" height="100%">
|
||||
<el-table-column label="ID" prop="id" align="center" width="70" />
|
||||
|
||||
<el-table-column label="规则名称" prop="ruleName" min-width="160" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="适用对象类型" align="center" width="130">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.targetType === 1" type="warning" size="small">单台设备</el-tag>
|
||||
<el-tag v-else-if="props.row.targetType === 2" type="primary" size="small">设备标签</el-tag>
|
||||
<el-tag v-else-if="props.row.targetType === 3" type="success" size="small">机型/配置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="匹配标识值" prop="targetValue" align="center" width="160" show-overflow-tooltip>
|
||||
<template #default="props">
|
||||
<span v-if="props.row.targetType === 3">
|
||||
{{ props.row.targetValue === '1' ? '医疗版本(1)' : props.row.targetValue === '2' ? '非医疗版本(2)' : props.row.targetValue }}
|
||||
</span>
|
||||
<el-tag v-else-if="props.row.targetType === 2" size="small" effect="plain">{{ props.row.targetValue }}</el-tag>
|
||||
<span v-else>{{ props.row.targetValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="覆盖病症" prop="symptomsName" min-width="150" show-overflow-tooltip>
|
||||
<template #default="props">
|
||||
<span>{{ props.row.symptomsName || `病症ID: ${props.row.symptomId}` }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="定制超声 (ul)" prop="ul" align="center" width="110">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.ul" size="small">{{ props.row.ul }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">继承默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="定制脉幅 (pl)" prop="pl" align="center" width="110">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.pl" size="small">{{ props.row.pl }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">继承默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="定制脉宽 (pw)" prop="pw" align="center" width="110">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.pw" size="small">{{ props.row.pw }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">继承默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="定制频率 (fr)" prop="fr" align="center" width="110">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.fr" size="small">{{ props.row.fr }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">继承默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="定制时长 (tl)" prop="tl" align="center" width="110">
|
||||
<template #default="props">
|
||||
<el-tag v-if="props.row.tl" size="small">{{ props.row.tl }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">继承默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="优先级" prop="priority" align="center" width="80" />
|
||||
|
||||
<el-table-column label="状态" align="center" width="90">
|
||||
<template #default="props">
|
||||
<el-switch
|
||||
v-model="props.row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(props.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="props">
|
||||
<span class="pointer active" @click="handleEditClick(props.row)">编辑</span>
|
||||
<el-divider direction="vertical" />
|
||||
<span class="pointer active" style="color: #f56c6c;" @click="handleDeleteClick(props.row)">删除</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</main>
|
||||
|
||||
<footer class="footer p-2 pr-4">
|
||||
<el-pagination
|
||||
v-model:current-page="queryForm.page"
|
||||
:page-size="queryForm.limit"
|
||||
layout="->, prev, pager, next, jumper, total"
|
||||
:total="tableData.total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<!-- 编辑/新建弹窗 -->
|
||||
<custom-params-form ref="customParamsFormRef" :title="dialogTitle" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, getCurrentInstance, onMounted } from 'vue'
|
||||
import CustomParamsForm from '@/components/admin/physio/CustomParamsForm.vue'
|
||||
import { getRulePage, deleteRule, changeRuleStatus } from '@/apis/admin/deviceTreatRule'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogTitle = ref('添加定制治疗参数规则')
|
||||
const customParamsFormRef = ref()
|
||||
|
||||
const queryForm = reactive({
|
||||
ruleName: '',
|
||||
targetType: '',
|
||||
status: '',
|
||||
page: 1,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
const tableData = reactive({
|
||||
list: [],
|
||||
total: 0
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
queryForm.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryForm.ruleName = ''
|
||||
queryForm.targetType = ''
|
||||
queryForm.status = ''
|
||||
queryForm.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const handlePageChange = val => {
|
||||
queryForm.page = val
|
||||
getList()
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRulePage({
|
||||
currentPage: queryForm.page,
|
||||
pageSize: queryForm.limit,
|
||||
ruleName: queryForm.ruleName || undefined,
|
||||
targetType: queryForm.targetType !== '' ? queryForm.targetType : undefined,
|
||||
status: queryForm.status !== '' ? queryForm.status : undefined
|
||||
})
|
||||
const { code, data } = res
|
||||
if (code === 200 || data) {
|
||||
tableData.list = data.records || []
|
||||
tableData.total = data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddClick = () => {
|
||||
dialogTitle.value = '添加定制治疗参数规则'
|
||||
customParamsFormRef.value.show()
|
||||
}
|
||||
|
||||
const handleEditClick = row => {
|
||||
dialogTitle.value = '编辑定制治疗参数规则'
|
||||
customParamsFormRef.value.show(row)
|
||||
}
|
||||
|
||||
const handleStatusChange = async row => {
|
||||
try {
|
||||
const res = await changeRuleStatus(row.id, row.status)
|
||||
const { code, message } = res
|
||||
if (code !== 200 && code !== undefined && res !== true) {
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
return proxy.$message.error(message || '状态切换失败')
|
||||
}
|
||||
proxy.$message.success('状态已更新')
|
||||
} catch (error) {
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = row => {
|
||||
proxy.$confirm(`确认删除规则【${row.ruleName}】吗?`, '删除警告', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteRule(row.id)
|
||||
const { code, message } = res
|
||||
if (code !== 200 && code !== undefined && res !== true) {
|
||||
return proxy.$message.error(message || '删除失败')
|
||||
}
|
||||
proxy.$message.success('删除成功')
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header,
|
||||
.main,
|
||||
.footer {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-radius: 5px;
|
||||
|
||||
.el-input {
|
||||
width: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
.el-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user