From 36528b6e0c365b072cc887497b2a679728a7818a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Wed, 26 Aug 2026 23:14:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=8E=A8=E9=80=81=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + refer/BiddingSupplierAccountApiCodec.java | Bin 0 -> 9540 bytes refer/gr-table-plus.vue | 161 ------- refer/gr-table-simple.vue | 94 ---- refer/gr-table.vue | 117 ----- refer/说明.txt | 13 + .../src/main/resources/application.yml | 12 + .../controller/BizExpertController.java | 11 + .../controller/BizProjectController.java | 24 +- .../SupplierAccountPullScheduler.java | 106 +++++ .../service/impl/BizMeetingServiceImpl.java | 16 +- .../service/impl/BizPersonServiceImpl.java | 13 +- .../supplier/SupplierAccountApiCodec.java | 73 ++++ .../mapper/business/BizProjectPlanMapper.xml | 4 +- ry-vue3/.env.development | 4 +- ry-vue3/src/components/OssFileUploader.vue | 24 +- ry-vue3/src/components/PortalNavbar.vue | 410 ++++++++++++++++++ ry-vue3/src/utils/oss.js | 31 +- ry-vue3/src/views/auth/Login.vue | 2 + ry-vue3/src/views/doctor/Meetings.vue | 2 +- ry-vue3/src/views/doctor/Projects.vue | 2 +- .../executor-people/ExecutorPersonNew.vue | 8 +- ry-vue3/src/views/executor/Meetings.vue | 4 +- ry-vue3/src/views/executor/Projects.vue | 2 +- ry-vue3/src/views/manager/Projects.vue | 46 +- ry-vue3/src/views/meetings/MeetingDetail.vue | 260 ++++++----- ry-vue3/src/views/meetings/Meetings.vue | 4 +- ry-vue3/src/views/portal/Home.vue | 122 +----- ry-vue3/src/views/portal/Publicity.vue | 63 +-- ry-vue3/src/views/portal/PublicityDetail.vue | 56 +-- .../src/views/portal/SpecialPlanDetail.vue | 5 + .../views/sponsor-people/SponsorPersonNew.vue | 8 +- ry-vue3/src/views/sponsor/Meetings.vue | 2 +- ry-vue3/src/views/sponsor/Projects.vue | 2 +- ry-vue3/src/views/sponsor/SponsorProjects.vue | 2 +- ry-vue3/vite.config.js | 8 +- 36 files changed, 933 insertions(+), 779 deletions(-) create mode 100644 refer/BiddingSupplierAccountApiCodec.java delete mode 100644 refer/gr-table-plus.vue delete mode 100644 refer/gr-table-simple.vue delete mode 100644 refer/gr-table.vue create mode 100644 refer/说明.txt create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountApiCodec.java create mode 100644 ry-vue3/src/components/PortalNavbar.vue diff --git a/.gitignore b/.gitignore index dab9666..efeb494 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ bin/ *.swp *.swo *~ +*.zip .gradle/ *.iml diff --git a/refer/BiddingSupplierAccountApiCodec.java b/refer/BiddingSupplierAccountApiCodec.java new file mode 100644 index 0000000000000000000000000000000000000000..4423255fb796210d6159e6c08eda9e2d86afcc5b GIT binary patch literal 9540 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 Xs!PiRw7|MFw34w53;W{6H2Qx4YFa@+ literal 0 HcmV?d00001 diff --git a/refer/gr-table-plus.vue b/refer/gr-table-plus.vue deleted file mode 100644 index cc3d4d5..0000000 --- a/refer/gr-table-plus.vue +++ /dev/null @@ -1,161 +0,0 @@ - - - \ No newline at end of file diff --git a/refer/gr-table-simple.vue b/refer/gr-table-simple.vue deleted file mode 100644 index 86b5547..0000000 --- a/refer/gr-table-simple.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - \ No newline at end of file diff --git a/refer/gr-table.vue b/refer/gr-table.vue deleted file mode 100644 index bb9b917..0000000 --- a/refer/gr-table.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - \ No newline at end of file diff --git a/refer/说明.txt b/refer/说明.txt new file mode 100644 index 0000000..2889570 --- /dev/null +++ b/refer/说明.txt @@ -0,0 +1,13 @@ +供应商系统需要增加一个接口 +请求参数:最后更新时间,支持分页。 +返回: +1、供应商账号注册信息(企业类型 企业名称 邮箱 账号 密码 公司地址 税号 联系人 联系电话) 启用 删除 禁用 标识 根据修改时间倒序排序返回。 +2、整个信息可以使用AES加密,提供解密算法,密码不可逆,提供提供算法进行比对。(因为数据返回都是加密的,且加密算法只提供给你们 不要到时候说我们接口信息安全泄露哈) + + +https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts?lastUpdatedTime=2026-08-01%2000:00:00&pageNum=1&pageSize=20 + +测试环境的密钥配置如下 +supplier-account-api-aes: + key-id: supplier-api-key + key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U=' \ No newline at end of file diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml index 1db5b3a..cdd4b46 100644 --- a/ry-api/ruoyi-admin/src/main/resources/application.yml +++ b/ry-api/ruoyi-admin/src/main/resources/application.yml @@ -59,6 +59,18 @@ ruoyi: camera: base-url: https://risingdoctor.com/camera/ +# 供应商账号接口 (拉取/解密) 配置 +supplier-account-api: + base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts + # 每页条数 + page-size: 20 + # 每次拉取最近 N 分钟更新的数据 + pull-minutes: 5 +# 供应商账号接口 AES 密钥 (数据解密用, 测试环境密钥) +supplier-account-api-aes: + key-id: supplier-api-key + key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U=' + # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java index 2f1b6be..f3763e5 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizExpertController.java @@ -48,6 +48,17 @@ public class BizExpertController extends BaseController { return success(bizExpertService.getByUserId(SecurityUtils.getUserId())); } + /** + * 按手机号查专家 (参会人 dialog 手机号放大镜回填用): 命中返回专家档案, 未命中返回 null + */ + @GetMapping("/byPhone/{phone}") + public AjaxResult getByPhone(@PathVariable String phone) + { + BizExpert q = new BizExpert(); + q.setPhone(phone); + List list = bizExpertService.selectList(q); + return success(list != null && !list.isEmpty() ? list.get(0) : null); + } /** * admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号) * 返回 SysUser (含明文 password 给前端 toast 用) diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java index abea150..91a8502 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizProjectController.java @@ -259,6 +259,9 @@ public class BizProjectController extends BaseController @PostMapping("/{projectId}/assigns") public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List assigns) { + // 已结题项目不能再分配 + BizProject project = requireAssignableProject(projectId); + if (assigns == null) assigns = new ArrayList<>(); bizProjectAssignService.validateSum(projectId, assigns); // #3 通知去重: 拉旧数据按 executionUnitId 索引, 同 (executionUnitId, sessions, amount) → 无变化 → 跳过 @@ -270,8 +273,7 @@ public class BizProjectController extends BaseController } bizProjectAssignService.deleteByProjectId(projectId); // #3 通知: 查一次项目名, 避免循环里重复查 DB - BizProject project = bizProjectService.getById(projectId); - String projectName = project != null ? project.getProjectName() : null; + String projectName = project.getProjectName(); for (BizProjectAssign a : assigns) { a.setProjectId(projectId); if (a.getStatus() == null) a.setStatus("0"); @@ -331,6 +333,15 @@ public class BizProjectController extends BaseController try { return Long.parseLong(s); } catch (NumberFormatException e) { return null; } } + /** 校验项目存在且未结题 — 已结题项目禁止一切分配操作, 返回项目供取项目名. */ + private BizProject requireAssignableProject(Long projectId) { + if (projectId == null) throw new ServiceException("项目不存在"); + BizProject p = bizProjectService.getById(projectId); + if (p == null) throw new ServiceException("项目不存在"); + if ("1".equals(p.getIsFinished())) throw new ServiceException("已结题项目不能分配"); + return p; + } + @Log(title = "项目执行方分配", businessType = BusinessType.DELETE) @DeleteMapping("/{projectId}/assigns") public AjaxResult clearAssigns(@PathVariable("projectId") Long projectId) @@ -406,6 +417,8 @@ public class BizProjectController extends BaseController if (body.getProjectId() == null) { return error("projectId 必填"); } + // 已结题项目不能分配 + BizProject project = requireAssignableProject(parseProjectId(body.getProjectId())); java.util.List mids = body.getMonitorUserIds(); if (mids == null || mids.isEmpty()) { // 向后兼容: 单值 monitorUserId @@ -430,8 +443,7 @@ public class BizProjectController extends BaseController // 通知被分配的监察员 (新增 / 说明或积分变化才发). projectId 在 sponsor_assign 是 String, 转 Long 查主表 Long projectIdLong = parseProjectId(body.getProjectId()); - BizProject project = projectIdLong != null ? bizProjectService.getById(projectIdLong) : null; - String projectName = project != null ? project.getProjectName() : null; + String projectName = project.getProjectName(); for (Long mid : mids) { if (mid == null) continue; BizProjectSponsorAssign old = oldByMonitor.get(mid); @@ -467,6 +479,8 @@ public class BizProjectController extends BaseController if (body.getProjectId() == null) { return error("projectId 必填"); } + // 已结题项目不能分配 + requireAssignableProject(parseProjectId(body.getProjectId())); java.util.List sids = body.getStaffUserIds(); if (sids == null || sids.isEmpty()) { // 向后兼容: 单值 staffUserId @@ -532,6 +546,8 @@ public class BizProjectController extends BaseController if (body.getProjectId() == null || body.getMonitorUserId() == null) { throw new IllegalArgumentException("projectId / monitorUserId 必填"); } + // 已结题项目不能分配 (抛 ServiceException → 本循环 catch 计入 errors) + requireAssignableProject(parseProjectId(body.getProjectId())); body.setCreateBy(loginName); body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(loginUid)); // 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化 diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java new file mode 100644 index 0000000..c1ce976 --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/scheduler/SupplierAccountPullScheduler.java @@ -0,0 +1,106 @@ +package com.ruoyi.business.scheduler; + +import java.net.URLEncoder; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.business.supplier.SupplierAccountApiCodec; +import com.ruoyi.common.utils.http.HttpUtils; + +import lombok.extern.slf4j.Slf4j; + +/** + * 供应商账号数据拉取调度器: 每分钟拉取最近 5 分钟更新的账号, 解密后打印. + *

+ * 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts} + * 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回. + * 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密. + *

+ * 说明: 只打印不落库 (后续需要持久化时再扩展). + */ +@Slf4j +@Component +public class SupplierAccountPullScheduler +{ + private static final String KEY = "supplier-account-api-aes.key"; + private static final String BASE_URL = "supplier-account-api.base-url"; + private static final String PAGE_SIZE = "supplier-account-api.page-size"; + private static final String PULL_MINUTES = "supplier-account-api.pull-minutes"; + + private static final String DEFAULT_BASE_URL = + "https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts"; + + private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private Environment env; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Scheduled(fixedRate = 60_000, initialDelay = 30_000) + public void pullAccounts() + { + try + { + String key = env.getProperty(KEY); + if (key == null || key.isEmpty()) + { + log.warn("[SupplierAccountPull] 未配置 {} , 跳过", KEY); + return; + } + String baseUrl = env.getProperty(BASE_URL, DEFAULT_BASE_URL); + int pageSize = env.getProperty(PAGE_SIZE, Integer.class, 20); + int pullMinutes = env.getProperty(PULL_MINUTES, Integer.class, 5*24*60*60); + String lastUpdatedTime = LocalDateTime.now().minusMinutes(pullMinutes).format(TIME_FMT); + + int pageNum = 1; + int fetched = 0; + while (true) + { + String param = "lastUpdatedTime=" + URLEncoder.encode(lastUpdatedTime, "UTF-8") + + "&pageNum=" + pageNum + "&pageSize=" + pageSize; + String resp = HttpUtils.sendGet(baseUrl, param); + if (resp == null || resp.isEmpty()) + { + break; + } + JsonNode root = objectMapper.readTree(resp); + String encrypted = root.path("data").asText(""); + if (encrypted.isEmpty()) + { + log.info("[SupplierAccountPull] page={} 无 data 字段 (code={}, msg={}), 结束", + pageNum, root.path("code").asText(), root.path("msg").asText()); + break; + } + + String plain = SupplierAccountApiCodec.decrypt(encrypted, key); + JsonNode inner = objectMapper.readTree(plain); + int total = inner.path("total").asInt(0); + JsonNode rows = inner.path("rows"); + int size = rows.isArray() ? rows.size() : 0; + fetched += size; + + // 只打印即可: 整页明文 JSON 打出来 (供观察/后续落库) + log.info("[SupplierAccountPull] page={} total={} 本页={} 明文: {}", pageNum, total, size, plain); + + if (size == 0 || fetched >= total) + { + break; + } + pageNum++; + } + log.info("[SupplierAccountPull] 本轮完成, 共 {} 条", fetched); + } + catch (Exception e) + { + log.warn("[SupplierAccountPull] 拉取失败 (跳过, 下分钟再试)", e); + } + } +} diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java index c56fd4e..aa9b06a 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingServiceImpl.java @@ -3,7 +3,9 @@ package com.ruoyi.business.service.impl; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.business.domain.BizMeeting; @@ -67,7 +69,19 @@ public class BizMeetingServiceImpl implements IBizMeetingService if (entity.getServiceAuditStage() == null || entity.getServiceAuditStage().isEmpty()) { entity.setServiceAuditStage("NOT_SUBMITTED"); } - return bizMeetingMapper.insert(entity); + try { + return bizMeetingMapper.insert(entity); + } + catch (DuplicateKeyException e) { + // 入库时 meeting_id 撞库 (Redis 序列被重置/回退): 自动加 10 + 随机(0~10) 重新取一次, 不直接报错 + Long oldId = entity.getMeetingId(); + if (oldId == null) { + throw e; + } + long retryId = oldId + 10L + ThreadLocalRandom.current().nextInt(11); + entity.setMeetingId(retryId); + return bizMeetingMapper.insert(entity); + } } /** diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java index 8658ab1..bf5aff8 100644 --- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizPersonServiceImpl.java @@ -79,9 +79,14 @@ public class BizPersonServiceImpl implements IBizPersonService } // 1. 创建 sys_user 子账号 - // 继承主账号 role_type, 避免子账号被 sys_user.role_type DEFAULT 'executor' 覆盖 - // (之前不写 roleType 时, sponsor 主账号的子账号会被 DEFAULT 错位成 executor) + // role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色: + // 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员). + // 仅当 unitType 缺失时才回退到主账号 role_type 兜底. SysUser mainUser = mainUserId == null ? null : sysUserMapper.selectUserById(mainUserId); + String roleType = entity.getUnitType(); + if (roleType == null || roleType.isEmpty()) { + roleType = mainUser != null ? mainUser.getRoleType() : null; + } SysUser newUser = new SysUser(); newUser.setUserName(entity.getLoginUsername()); newUser.setNickName(entity.getName()); @@ -92,8 +97,8 @@ public class BizPersonServiceImpl implements IBizPersonService newUser.setParentUserId(mainUserId); newUser.setStatus("0"); newUser.setDelFlag("0"); - if (mainUser != null && mainUser.getRoleType() != null) { - newUser.setRoleType(mainUser.getRoleType()); + if (roleType != null) { + newUser.setRoleType(roleType); } newUser.setCreateBy(SecurityUtils.getUsername()); // mybatis useGeneratedKeys 自动回填 userId diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountApiCodec.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountApiCodec.java new file mode 100644 index 0000000..b96b2ef --- /dev/null +++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/supplier/SupplierAccountApiCodec.java @@ -0,0 +1,73 @@ +package com.ruoyi.business.supplier; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * 供应商账号接口 AES 解密工具 (对应 refer/BiddingSupplierAccountApiCodec). + *

+ * 接口返回的 {@code data} 字段格式: {@code BSA.v1...} + *

    + *
  • BSA — 固定前缀 (Bidding Supplier Account)
  • + *
  • v1 — 版本号
  • + *
  • keyId — 密钥标识, 对应配置 {@code supplier-account-api-aes.key-id} + * (单密钥场景忽略, 密钥旋转时按此取对应 key)
  • + *
  • nonce — 12 字节随机 IV, URL-safe Base64 编码 (无填充)
  • + *
  • ciphertext — 密文, URL-safe Base64 编码 (无填充, 末尾带 16 字节 GCM 认证标签)
  • + *
+ * 算法: AES-256/GCM/NoPadding (密钥 32 字节, GCM 认证标签 128 bit). + */ +public class SupplierAccountApiCodec +{ + private static final int GCM_TAG_BITS = 128; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + + private SupplierAccountApiCodec() {} + + /** + * 解密接口返回的 data 字段, 得到明文 JSON 字符串. + * + * @param data 接口返回的 data 字段 ({@code BSA.v1.keyId.nonce.ciphertext}) + * @param key AES 密钥 (Base64 编码, 解码后 32 字节) + */ + public static String decrypt(String data, String key) + { + if (data == null || data.isEmpty()) + { + return ""; + } + String[] parts = data.split("\\."); + if (parts.length < 5) + { + throw new IllegalArgumentException("供应商接口 data 格式非法 (期望 BSA.v1.keyId.nonce.ciphertext)"); + } + byte[] keyBytes = Base64.getDecoder().decode(key); + byte[] nonce = decodeBase64Url(parts[3]); + byte[] ciphertext = decodeBase64Url(parts[4]); + try + { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), + new GCMParameterSpec(GCM_TAG_BITS, nonce)); + return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); + } + catch (Exception e) + { + throw new IllegalStateException("供应商接口数据解密失败: " + e.getMessage(), e); + } + } + + /** URL-safe Base64 → bytes (兼容 '-'/'_' 且无填充) */ + private static byte[] decodeBase64Url(String s) + { + String b = s.replace('-', '+').replace('_', '/'); + while (b.length() % 4 != 0) + { + b += "="; + } + return Base64.getDecoder().decode(b); + } +} diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml index 77035d3..5e8ab7a 100644 --- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml +++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectPlanMapper.xml @@ -49,15 +49,17 @@ p.is_deleted = 0 + and p.plan_name like concat('%', #{planName}, '%') and p.plan_direction_id = #{planDirectionId} and p.plan_category = #{planCategory} + and p.project_form = #{projectForm} and p.submitter_id = #{submitterId} and p.status = #{status} - and p.remark = #{remark} + and p.remark like concat('%', #{remark}, '%') order by p.plan_id desc diff --git a/ry-vue3/.env.development b/ry-vue3/.env.development index 7c893ea..107ed66 100644 --- a/ry-vue3/.env.development +++ b/ry-vue3/.env.development @@ -1,2 +1,2 @@ -# 开发环境 API 基路径: 直连远程后端 (绕过 Vite proxy, 避免 HTTPS 代理卡住) -VITE_APP_BASE_API = 'https://risingdoctor.com/hg-api' +# 开发环境 API 基路径: 本地后端 (走 Vite proxy 转发到 localhost:8080) +VITE_APP_BASE_API = '/dev-api' diff --git a/ry-vue3/src/components/OssFileUploader.vue b/ry-vue3/src/components/OssFileUploader.vue index 4156914..8629f93 100644 --- a/ry-vue3/src/components/OssFileUploader.vue +++ b/ry-vue3/src/components/OssFileUploader.vue @@ -10,8 +10,12 @@ /> -->