diff --git a/deploy/scripts/setup_wsl_env.sh b/deploy/scripts/setup_wsl_env.sh new file mode 100644 index 0000000..a8c4afe --- /dev/null +++ b/deploy/scripts/setup_wsl_env.sh @@ -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 < page(UniPage 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 page = new Page<>(uniPage.getCurrentPage(), uniPage.getPageSize()); + LambdaQueryWrapper 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 resultPage = deviceTreatRuleMapper.selectPage(page, queryWrapper); + List records = resultPage.getRecords(); + if (CollectionUtils.isNotEmpty(records)) { + List symptomIds = records.stream() + .map(DeviceTreatRule::getSymptomId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(symptomIds)) { + List treatParameters = treatParameterMapper.selectBatchIds(symptomIds); + Map 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 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; + } +} diff --git a/server/admin/src/main/java/cn/unizk/controller/SysDeviceController.java b/server/admin/src/main/java/cn/unizk/controller/SysDeviceController.java index e7d0961..b4fb7d7 100644 --- a/server/admin/src/main/java/cn/unizk/controller/SysDeviceController.java +++ b/server/admin/src/main/java/cn/unizk/controller/SysDeviceController.java @@ -425,6 +425,39 @@ public class SysDeviceController { return true; } + @PostMapping("/setTags") + public boolean setTags(@RequestBody DeviceTagsVO vo) { + List deviceIds = vo.getDeviceIds(); + if (CollectionUtils.isEmpty(deviceIds)) { + return false; + } + List existingDevices = deviceService.listByIds(deviceIds); + List 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 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, diff --git a/server/admin/src/main/java/cn/unizk/service/GeneratePrService.java b/server/admin/src/main/java/cn/unizk/service/GeneratePrService.java index 6cae5af..98e7ffb 100644 --- a/server/admin/src/main/java/cn/unizk/service/GeneratePrService.java +++ b/server/admin/src/main/java/cn/unizk/service/GeneratePrService.java @@ -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 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); + } + } + } else { + PrDTO.PrItem prItem = fill(painNumber.trim(), device); + if (prItem != null) { prItems.add(prItem); } - prDTO.setPrItems(prItems); - }else { - PrDTO.PrItem prItem = fill(painNumber); - prItems.add(prItem); - prDTO.setPrItems(prItems); } + 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 rules = deviceTreatRuleMapper.selectList( + new LambdaQueryWrapper() + .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 rules, Device device) { + List tags = Collections.emptyList(); + if (StringUtils.isNotBlank(device.getDeviceTags())) { + tags = Arrays.asList(device.getDeviceTags().split(",")); + } + + List 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); + } + } } diff --git a/server/admin/src/main/java/cn/unizk/service/UdpServerService2.java b/server/admin/src/main/java/cn/unizk/service/UdpServerService2.java index 91d3470..4ff5a2e 100644 --- a/server/admin/src/main/java/cn/unizk/service/UdpServerService2.java +++ b/server/admin/src/main/java/cn/unizk/service/UdpServerService2.java @@ -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 prItems = prDTO.getPrItems(); if (CollectionUtils.isNotEmpty(prItems)){ diff --git a/server/admin/src/main/java/cn/unizk/service/impl/DeviceServiceImpl.java b/server/admin/src/main/java/cn/unizk/service/impl/DeviceServiceImpl.java index face7bb..4d0a621 100644 --- a/server/admin/src/main/java/cn/unizk/service/impl/DeviceServiceImpl.java +++ b/server/admin/src/main/java/cn/unizk/service/impl/DeviceServiceImpl.java @@ -57,6 +57,8 @@ public class DeviceServiceImpl extends ServiceImpl 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() diff --git a/server/admin/src/main/resources/application-local.yml b/server/admin/src/main/resources/application-local.yml new file mode 100644 index 0000000..f58f9f9 --- /dev/null +++ b/server/admin/src/main/resources/application-local.yml @@ -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 diff --git a/server/admin/src/main/resources/logback-spring.xml b/server/admin/src/main/resources/logback-spring.xml index b5d93a3..73c7624 100644 --- a/server/admin/src/main/resources/logback-spring.xml +++ b/server/admin/src/main/resources/logback-spring.xml @@ -27,8 +27,8 @@ - - + + diff --git a/server/common/src/main/java/cn/unizk/entity/po/Device.java b/server/common/src/main/java/cn/unizk/entity/po/Device.java index e7f4131..52bbd5f 100644 --- a/server/common/src/main/java/cn/unizk/entity/po/Device.java +++ b/server/common/src/main/java/cn/unizk/entity/po/Device.java @@ -106,6 +106,9 @@ public class Device { private Integer treatStatus; + @ExcelProperty(value = "设备标签") + private String deviceTags; - + @ExcelProperty(value = "机型品类") + private String modelCategory; } diff --git a/server/common/src/main/java/cn/unizk/entity/po/DeviceTreatRule.java b/server/common/src/main/java/cn/unizk/entity/po/DeviceTreatRule.java new file mode 100644 index 0000000..ecd3807 --- /dev/null +++ b/server/common/src/main/java/cn/unizk/entity/po/DeviceTreatRule.java @@ -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; +} diff --git a/server/common/src/main/java/cn/unizk/entity/vo/DeviceTagsVO.java b/server/common/src/main/java/cn/unizk/entity/vo/DeviceTagsVO.java new file mode 100644 index 0000000..6510c1c --- /dev/null +++ b/server/common/src/main/java/cn/unizk/entity/vo/DeviceTagsVO.java @@ -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 deviceIds; + + /** + * 标签列表(英文逗号分隔,如 "golf_vip,elderly_care") + */ + private String tags; + + /** + * 机型品类编码(可选) + */ + private String modelCategory; + + /** + * 设置模式:1-覆盖(默认),2-追加 + */ + private Integer mode = 1; +} diff --git a/server/common/src/main/java/cn/unizk/mapper/DeviceTreatRuleMapper.java b/server/common/src/main/java/cn/unizk/mapper/DeviceTreatRuleMapper.java new file mode 100644 index 0000000..2a4d8c2 --- /dev/null +++ b/server/common/src/main/java/cn/unizk/mapper/DeviceTreatRuleMapper.java @@ -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 { + +} diff --git a/tools/test_verify_custom_params.py b/tools/test_verify_custom_params.py new file mode 100644 index 0000000..bbafbe3 --- /dev/null +++ b/tools/test_verify_custom_params.py @@ -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() diff --git a/web/admin/src/apis/admin/device.js b/web/admin/src/apis/admin/device.js index b2c1ccc..2628482 100644 --- a/web/admin/src/apis/admin/device.js +++ b/web/admin/src/apis/admin/device.js @@ -18,6 +18,7 @@ const files = { export: 'exportDevices', remark: 'remark', frequency: 'getTopThree', + setTags: 'setTags', } /** diff --git a/web/admin/src/apis/admin/deviceTreatRule.js b/web/admin/src/apis/admin/deviceTreatRule.js new file mode 100644 index 0000000..e3cb78d --- /dev/null +++ b/web/admin/src/apis/admin/deviceTreatRule.js @@ -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 } + }) +} diff --git a/web/admin/src/components/admin/device/SetDeviceTags.vue b/web/admin/src/components/admin/device/SetDeviceTags.vue new file mode 100644 index 0000000..c61ec02 --- /dev/null +++ b/web/admin/src/components/admin/device/SetDeviceTags.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/web/admin/src/components/admin/physio/CustomParamsForm.vue b/web/admin/src/components/admin/physio/CustomParamsForm.vue new file mode 100644 index 0000000..ce6836d --- /dev/null +++ b/web/admin/src/components/admin/physio/CustomParamsForm.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/web/admin/src/router/admin/physio.js b/web/admin/src/router/admin/physio.js index 25f4d1b..44a8517 100644 --- a/web/admin/src/router/admin/physio.js +++ b/web/admin/src/router/admin/physio.js @@ -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') diff --git a/web/admin/src/views/admin/device/list.vue b/web/admin/src/views/admin/device/list.vue index d9735fa..e6d6442 100644 --- a/web/admin/src/views/admin/device/list.vue +++ b/web/admin/src/views/admin/device/list.vue @@ -77,6 +77,9 @@ 设备配置 + + 设置标签 + 设备清零 @@ -132,6 +135,23 @@ + + + + @@ -171,6 +191,7 @@ + +