From a14d95288cd5092e33799b391fc8f18319fb4c14 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: Mon, 31 Aug 2026 19:01:00 +0800
Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=89=80=E6=9C=89=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/resources/application-druid.yml | 5 +-
.../src/main/resources/application-prod.yml | 34 +++
.../src/main/resources/application-test.yml | 27 ++
.../src/main/resources/application.yml | 21 +-
.../BizMeetingAttendeeController.java | 56 +++-
.../controller/BizMeetingController.java | 4 +
.../BizMeetingInvoiceController.java | 26 +-
.../BizMeetingStageStatsController.java | 21 +-
.../controller/BizPersonController.java | 14 +-
.../controller/BizProjectController.java | 5 +-
.../controller/BizPublicController.java | 61 +++++
.../business/domain/BizMeetingInvoice.java | 24 ++
.../domain/vo/BizMeetingAttendeeImportVo.java | 4 +-
.../mapper/BizMeetingInvoiceMapper.java | 22 ++
.../com/ruoyi/business/ocr/ZipExtractor.java | 66 +++--
.../com/ruoyi/business/pdf/SealStamper.java | 84 ++++++
.../SupplierAccountPullScheduler.java | 2 +-
.../ruoyi/business/service/PdfService.java | 18 ++
.../ruoyi/business/service/StageDeriver.java | 8 +-
.../service/impl/BizAdminUserServiceImpl.java | 3 +-
.../impl/BizMeetingMaterialServiceImpl.java | 96 ++++++-
.../service/impl/BizSignServiceImpl.java | 42 ++-
.../service/impl/InvoiceOcrService.java | 25 +-
.../ruoyi/business/sms/AliyunSmsSender.java | 2 +-
.../business/BizMeetingInvoiceMapper.xml | 49 +++-
.../mapper/business/BizMeetingMapper.xml | 4 +
.../mapper/business/BizProjectMapper.xml | 1 +
.../ruoyi/common/utils/http/HttpUtils.java | 3 +
.../framework/config/SecurityConfig.java | 3 +
.../security/filter/LeaderReadOnlyFilter.java | 84 ++++++
ry-api/zhenghe.png | Bin 0 -> 19191 bytes
ry-h5/pages/signin/index.uvue | 4 +-
ry-vue3/.env.production | 4 +-
ry-vue3/.env.test | 5 +
ry-vue3/package.json | 4 +-
ry-vue3/src/api/system.js | 4 +-
ry-vue3/src/assets/login.jpg | Bin 1507581 -> 489615 bytes
ry-vue3/src/components/OssFileUploader.vue | 16 +-
ry-vue3/src/layout/AdminLayout.vue | 14 +-
ry-vue3/src/router/index.js | 14 +-
ry-vue3/src/utils/meetingStage.js | 24 +-
ry-vue3/src/views/admin/Users.vue | 4 +-
ry-vue3/src/views/auth/Login.vue | 8 +-
ry-vue3/src/views/leader/Account.vue | 100 +++++++
ry-vue3/src/views/leader/Home.vue | 153 +++++++++++
ry-vue3/src/views/manager/Plans.vue | 14 +-
ry-vue3/src/views/manager/Projects.vue | 59 ++--
ry-vue3/src/views/manager/ProjectsNew.vue | 32 +--
ry-vue3/src/views/meetings/MeetingDetail.vue | 251 +++++++++++-------
ry-vue3/src/views/meetings/Meetings.vue | 37 ++-
ry-vue3/src/views/portal/Home.vue | 61 ++++-
ry-vue3/src/views/portal/PublicityDetail.vue | 18 +-
ry-vue3/vite.config.js | 2 +-
53 files changed, 1356 insertions(+), 286 deletions(-)
create mode 100644 ry-api/ruoyi-admin/src/main/resources/application-prod.yml
create mode 100644 ry-api/ruoyi-admin/src/main/resources/application-test.yml
create mode 100644 ry-api/ruoyi-business/src/main/java/com/ruoyi/business/pdf/SealStamper.java
create mode 100644 ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/LeaderReadOnlyFilter.java
create mode 100644 ry-api/zhenghe.png
create mode 100644 ry-vue3/.env.test
create mode 100644 ry-vue3/src/views/leader/Account.vue
create mode 100644 ry-vue3/src/views/leader/Home.vue
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-druid.yml b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
index 2398376..9ed08ce 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application-druid.yml
@@ -4,10 +4,7 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
- master:
- url: jdbc:mysql://mmos.uvwcloud.com:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- username: root
- password: Qljl1rh_
+ # master 数据源 url/username/password 按环境拆分到 application-test.yml / application-prod.yml
slave:
enabled: false
url:
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-prod.yml b/ry-api/ruoyi-admin/src/main/resources/application-prod.yml
new file mode 100644
index 0000000..125515b
--- /dev/null
+++ b/ry-api/ruoyi-admin/src/main/resources/application-prod.yml
@@ -0,0 +1,34 @@
+# 生产环境数据源 (47.95.12.80 / hegui)
+# 生产端口 8082 (测试端口 8080 为 application.yml 默认值)
+server:
+ port: 8082
+
+spring:
+ datasource:
+ druid:
+ master:
+ url: jdbc:mysql://47.95.12.80:3306/hegui?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: hegui
+ password: C55zwt5L2TeB4kLB
+
+# 生产环境外部接口地址
+ruoyi:
+ sms:
+ esignBaseUrl: https://hegui.bahim.org.cn
+ # 扫码拍照 (ry-h5 相机网页地址, 前端二维码目标 URL)
+ camera:
+ base-url: https://hegui.bahim.org.cn/camera/
+
+supplier-account-api:
+# base-url: https://zbsupplier.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
+ base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
+
+supplier-account-api-aes:
+ key-id: supplier-api-key
+ key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U='
+
+bidding-api:
+# notice-list-url: https://zbpublic.guojustar.com/portal-api/bidding/public/notice/list
+# detail-url: https://zbpublic.guojustar.com/#/detail/
+ notice-list-url: https://zbpublictest.guojustar.com/portal-api/bidding/public/notice/list
+ detail-url: https://zbpublictest.guojustar.com/#/detail/
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-test.yml b/ry-api/ruoyi-admin/src/main/resources/application-test.yml
new file mode 100644
index 0000000..10211c1
--- /dev/null
+++ b/ry-api/ruoyi-admin/src/main/resources/application-test.yml
@@ -0,0 +1,27 @@
+# 测试环境数据源 (mmos.uvwcloud.com / guoju0808)
+spring:
+ datasource:
+ druid:
+ master:
+ url: jdbc:mysql://mmos.uvwcloud.com:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: root
+ password: Qljl1rh_
+
+# 测试环境外部接口地址
+ruoyi:
+ sms:
+ esignBaseUrl: https://risingdoctor.com/hg
+ # 扫码拍照 (ry-h5 相机网页地址, 前端二维码目标 URL)
+ camera:
+ base-url: https://risingdoctor.com/camera/
+
+supplier-account-api:
+ base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
+
+supplier-account-api-aes:
+ key-id: supplier-api-key
+ key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U='
+
+bidding-api:
+ notice-list-url: https://zbpublictest.guojustar.com/portal-api/bidding/public/notice/list
+ detail-url: https://zbpublictest.guojustar.com/#/detail/
diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml
index fd4f0a5..60135b7 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application.yml
@@ -33,7 +33,6 @@ ruoyi:
signName: 北京整合医学学会
template: SMS_291440833
esignTemplate: SMS_512040098
- esignBaseUrl: https://hegui.bahim.org.cn
endpoint: dysmsapi.aliyuncs.com
regionId: cn-hangzhou
# 发票 OCR (本地 Java 识别, PaddleOCR ONNX Runtime, 替代原 ry-ocr Python 微服务)
@@ -55,21 +54,15 @@ ruoyi:
upload:
max-mb: 20
pdf-dpi: 150
- # 扫码拍照 (ry-h5 相机网页地址, 前端二维码目标 URL)
- camera:
- base-url: https://risingdoctor.com/camera/
-
-# 供应商账号接口 (拉取/解密) 配置
+# 供应商账号接口 (拉取/解密) 配置 (base-url 按环境拆分到 application-test/prod.yml)
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='
+# 供应商账号接口 AES 密钥 (supplier-account-api-aes.key 按环境拆分到 application-test/prod.yml)
+
+# 招标公告查询接口 (notice-list-url / detail-url 按环境拆分到 application-test/prod.yml)
# 开发环境配置
server:
@@ -103,7 +96,11 @@ spring:
# 国际化资源文件路径
basename: i18n/messages
profiles:
- active: druid
+ # 默认 prod 环境; 本地测试用 --spring.profiles.active=test (group 自动展开为 druid,test)
+ active: prod
+ group:
+ test: druid,test
+ prod: druid,prod
# 文件上传
servlet:
multipart:
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
index 083815c..f5ba328 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingAttendeeController.java
@@ -72,10 +72,18 @@ public class BizMeetingAttendeeController extends BaseController {
/**
* 管理端: 某会议的全部参会人 (MeetingDetail 参会人 CRUD 用).
* 返回全字段, 前端按需展示.
+ *
sponsor (监督方) 只读且敏感信息脱敏: 姓名/手机号/银行卡号/账户名称/身份证号在后端打码,
+ * 明文不下发 (前端遮罩只是显示层, 不是安全边界).
*/
@GetMapping("/list/{meetingId}")
public AjaxResult listByMeetingId(@PathVariable("meetingId") Long meetingId) {
- return success(attendeeService.selectByMeetingId(meetingId));
+ List list = attendeeService.selectByMeetingId(meetingId);
+ if (isSponsor()) {
+ for (BizMeetingAttendee a : list) {
+ maskForSponsor(a);
+ }
+ }
+ return success(list);
}
/**
@@ -324,4 +332,50 @@ public class BizMeetingAttendeeController extends BaseController {
}
}
+ /** 当前登录用户是否为 sponsor (监督方只读, 敏感信息需脱敏) */
+ private boolean isSponsor() {
+ try {
+ return "sponsor".equals(SecurityUtils.getLoginUser().getUser().getRoleType());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /** sponsor 脱敏: 姓名/手机号/银行卡号/账户名称/身份证号 (对齐 desensitizeHtml 口径) */
+ private void maskForSponsor(BizMeetingAttendee a) {
+ if (a == null) return;
+ a.setName(maskName(a.getName()));
+ a.setPhone(maskPhone(a.getPhone()));
+ a.setBankCard(maskBankCard(a.getBankCard()));
+ a.setAccountName(maskName(a.getAccountName()));
+ a.setIdCard(maskIdCard(a.getIdCard()));
+ }
+
+ /** 姓名脱敏: 保留首字 (姓), 其余打码 (张三 → 张*) */
+ private String maskName(String v) {
+ if (v == null) return null;
+ String s = v.trim();
+ if (s.isEmpty()) return v;
+ if (s.length() <= 1) return "*";
+ return s.charAt(0) + "*".repeat(s.length() - 1);
+ }
+
+ /** 手机号脱敏: 前3 + **** + 后4 */
+ private String maskPhone(String v) {
+ if (v == null || v.length() != 11) return v;
+ return v.substring(0, 3) + "****" + v.substring(7);
+ }
+
+ /** 银行卡号脱敏: 前4 + ******** + 后4 */
+ private String maskBankCard(String v) {
+ if (v == null || v.length() < 8) return v;
+ return v.substring(0, 4) + "********" + v.substring(v.length() - 4);
+ }
+
+ /** 身份证号脱敏: 前6 + ******** + 后4 */
+ private String maskIdCard(String v) {
+ if (v == null || v.length() != 18) return v;
+ return v.substring(0, 6) + "********" + v.substring(14);
+ }
+
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java
index 6992aba..197c58c 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingController.java
@@ -111,6 +111,10 @@ public class BizMeetingController extends BaseController {
else if ("manager".equals(roleType)) {
bizMeeting.getParams().put("managerCreateUserId", uid);
}
+ // 项目负责人(leader) 数据权限: 只看"本人负责的项目"下的会议 (project_id ∈ lead_user_id = 自己)
+ else if ("leader".equals(roleType)) {
+ bizMeeting.getParams().put("leaderUserId", uid);
+ }
startPage();
List list = bizMeetingService.selectList(bizMeeting);
return getDataTable(list);
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingInvoiceController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingInvoiceController.java
index 798009b..c72beb1 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingInvoiceController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingInvoiceController.java
@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.business.service.IBizMeetingInvoiceService;
import com.ruoyi.business.service.impl.InvoiceOcrService;
/**
@@ -11,7 +12,7 @@ import com.ruoyi.business.service.impl.InvoiceOcrService;
*
* v3 变更:
* - 端点改为"提交即返回 SUBMITTED", OCR 在后台 ExecutorService 跑
- * - 新增 isZip 字段: true → zip 路径(解压 → 重传 OSS → 写多行 invoice)
+ * - zip 由后端按 ossUrl 后缀判断 (解压 → 重传 OSS → 写多行 invoice), 前端无需传 isZip
* - 新增 oldMaterialId 字段: 替换场景, 后端先清旧 invoice + material.amount=0
* - 不再走旧的同步 recognizeAndSave, 已被 submitRecognition 取代
*/
@@ -23,14 +24,16 @@ public class BizMeetingInvoiceController extends BaseController
@Autowired
private InvoiceOcrService invoiceOcrService;
+ @Autowired
+ private IBizMeetingInvoiceService invoiceService;
+
/**
* 提交发票识别 (后台执行, 立即返回)
*
* body: {
* "materialId": 123,
* "meetingId": 456,
- * "ossUrl": "https://...",
- * "isZip": false, // 新增
+ * "ossUrl": "https://...", // .zip 结尾走 zip 解压, 否则按单文件
* "oldMaterialId": null // 新增, 替换时携带原 materialId
* }
*/
@@ -41,19 +44,28 @@ public class BizMeetingInvoiceController extends BaseController
body.getMaterialId(),
body.getMeetingId(),
body.getOssUrl(),
- body.isZip(),
body.getOldMaterialId());
return success(r);
}
+ /**
+ * 查询某会议下所有发票明细 (会务材料 tab 顶部「发票表」展示)
+ *
+ * 返回 BizMeetingInvoice 列表, 含 materialSubType / materialFileName (join biz_meeting_material 带出),
+ * 供前端按所属材料类型渲染发票表.
+ */
+ @GetMapping("/list")
+ public AjaxResult list(@RequestParam("meetingId") Long meetingId)
+ {
+ return success(invoiceService.selectByMeetingId(meetingId));
+ }
+
/** request body wrapper */
public static class RecognizeBody
{
private Long materialId;
private Long meetingId;
private String ossUrl;
- /** v3 新增: true=zip 路径(解压遍历), false=单文件路径 */
- private boolean isZip;
/** v3 新增: 替换场景携带的旧 materialId, 后端先 DELETE invoice WHERE material_id=old + material.amount=0 */
private Long oldMaterialId;
@@ -63,8 +75,6 @@ public class BizMeetingInvoiceController extends BaseController
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getOssUrl() { return ossUrl; }
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
- public boolean isZip() { return isZip; }
- public void setZip(boolean zip) { isZip = zip; }
public Long getOldMaterialId() { return oldMaterialId; }
public void setOldMaterialId(Long oldMaterialId) { this.oldMaterialId = oldMaterialId; }
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingStageStatsController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingStageStatsController.java
index 6c20d66..2b647b2 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingStageStatsController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizMeetingStageStatsController.java
@@ -31,26 +31,37 @@ public class BizMeetingStageStatsController extends BaseController
private SysUserMapper sysUserMapper;
/**
- * 按 current_stage 分组计数, 只做 sponsor 数据权限隔离 (MAIN=本支持单位全部, SUB=自己负责的项目).
+ * 按 current_stage 分组计数, 做 sponsor / leader 数据权限隔离 (sponsor MAIN=本支持单位全部, SUB=自己负责的项目; leader=本人负责的项目).
* 返回 { stage: cnt, ... }, 前端据此算: 未执行=NOT_STARTED, 已执行=排除 NOT_STARTED/IN_PROGRESS 的其余 stage (含 FROZEN).
- * 其他角色的统计端点单独实现, 此端点非 sponsor 调用返回空 map (不泄露全量数据).
+ * 其他角色的统计端点单独实现, 此端点非 sponsor/leader 调用返回空 map (不泄露全量数据).
*/
@GetMapping("/stageStats")
public AjaxResult stageStats() {
Map result = new HashMap<>();
- if (!"sponsor".equals(SecurityUtils.getLoginUser().getUser().getRoleType())) {
+ String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
+ if (!"sponsor".equals(roleType) && !"leader".equals(roleType)) {
return success(result);
}
BizMeeting q = new BizMeeting();
Long uid = SecurityUtils.getUserId();
+ if ("leader".equals(roleType)) {
+ // 项目负责人: 统计范围 = 本人负责的项目 (lead_user_id = 自己)
+ q.getParams().put("leaderUserId", uid);
+ return success(doStats(q));
+ }
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
if (current != null && "SUB".equals(current.getAccountType())) {
q.getParams().put("monitorUserId", uid);
} else {
q.getParams().put("sponsorAdminUserId", uid);
}
+ return success(doStats(q));
+ }
+
+ /** 按 current_stage 分组计数, 结果 key 大小写随 JDBC 驱动, 做大小写不敏感匹配, 避免"统计恒 0". */
+ private Map doStats(BizMeeting q) {
+ Map result = new HashMap<>();
for (Map r : bizMeetingService.selectStageStats(q)) {
- // resultType=map 的 key 大小写随 JDBC 驱动, 做大小写不敏感匹配, 避免"统计恒 0"
String stage = null;
long cnt = 0L;
for (Map.Entry e : r.entrySet()) {
@@ -66,6 +77,6 @@ public class BizMeetingStageStatsController extends BaseController
result.put(stage, cnt);
}
}
- return success(result);
+ return result;
}
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java
index c74b6ae..4aa0c70 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPersonController.java
@@ -48,7 +48,7 @@ public class BizPersonController extends BaseController
@GetMapping("/list")
public TableDataInfo list(BizPerson bizPerson)
{
- // 当前登录用户是 MAIN 业务账号 → 自动注入 subUserIds, 隔离到本主账号的子账号对应人员
+ // 当前登录用户是 MAIN 业务账号 → 自动注入 subUserIds, 隔离到「本主账号自己 + 其子账号」对应人员
Long currentUserId = getUserId();
SysUser currentUser = currentUserId == null ? null : sysUserMapper.selectUserById(currentUserId);
if (currentUser != null
@@ -56,10 +56,14 @@ public class BizPersonController extends BaseController
&& currentUser.getRoleType() != null
&& BUSINESS_MAIN_ROLES.contains(currentUser.getRoleType())) {
List subUserIds = sysUserMapper.selectUserIdsByParent(currentUserId);
- // 即使没子账号也强制注入, 避免漏出 sys_user.parent_user_id=NULL 的孤立 person
- bizPerson.getParams().put("subUserIds", subUserIds == null || subUserIds.isEmpty()
- ? java.util.Collections.singletonList(-1L)
- : subUserIds);
+ // 隔离范围 = 主账号自己 + 子账号. 主账号自己也要能看到自己的 biz_person
+ // (公益支持/项目执行意向回显 工作单位/部门/职务 用), 同时仍不漏出 parent_user_id=NULL 的孤立 person
+ List visibleUserIds = new ArrayList<>();
+ visibleUserIds.add(currentUserId);
+ if (subUserIds != null) {
+ visibleUserIds.addAll(subUserIds);
+ }
+ bizPerson.getParams().put("subUserIds", visibleUserIds);
}
startPage();
List list = bizPersonService.selectList(bizPerson);
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 ba271e8..7fe67fb 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
@@ -106,10 +106,12 @@ public class BizProjectController extends BaseController
@GetMapping("/list")
public TableDataInfo list(BizProject bizProject)
{
- // 合规人员(manager): 只看本人创建的项目 (create_user_id = 当前用户); admin 不限制
+ // 数据权限: manager 只看本人创建(create_user_id), leader 只看本人负责(lead_user_id), admin 不限制
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
if ("manager".equals(roleType)) {
bizProject.getParams().put("createUserId", SecurityUtils.getUserId());
+ } else if ("leader".equals(roleType)) {
+ bizProject.getParams().put("leaderUserId", SecurityUtils.getUserId());
}
startPage();
List list = bizProjectService.selectList(bizProject);
@@ -192,6 +194,7 @@ public class BizProjectController extends BaseController
List list = bizProjectService.selectExecutorList(bizProject);
return getDataTable(list);
}
+
@GetMapping("/{projectId}")
public AjaxResult getInfo(@PathVariable("projectId") Long projectId)
{
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
index 92ba2a6..471b997 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizPublicController.java
@@ -3,13 +3,21 @@ package com.ruoyi.business.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.net.URLEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.core.env.Environment;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.http.HttpUtils;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.ruoyi.business.domain.BizArticle;
import com.ruoyi.business.domain.BizProject;
import com.ruoyi.business.domain.BizSupportLetter;
@@ -37,6 +45,10 @@ public class BizPublicController extends BaseController {
@Autowired private IBizArticleService articleService;
@Autowired private IBizSpecialPlanService specialPlanService;
+ @Autowired private Environment environment;
+ private static final Logger log = LoggerFactory.getLogger(BizPublicController.class);
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
/**
* 按类型获取已发布的协议/政策 (供注册页底部链接打开)
* type: agreement | privacy
@@ -92,6 +104,55 @@ public class BizPublicController extends BaseController {
return success(projectService.getById(projectId));
}
+ /**
+ * 招标公告查询: 按项目编号查外部招标平台的 noticeId, 返回详情跳转 URL.
+ * 数据源: GET {bidding-api.notice-list-url}?pageNum=1&pageSize=200&keyword={projectNo}&projectType=10¬iceType=10
+ * 取响应 rows[0].noticeId, 拼接 {bidding-api.detail-url}{noticeId} 返回, 前端 window.open 跳转.
+ */
+ @GetMapping("/biddingNotice")
+ public AjaxResult biddingNotice(@RequestParam String projectNo)
+ {
+ String listUrl = environment.getProperty("bidding-api.notice-list-url",
+ "https://zbpublic.guojustar.com/portal-api/bidding/public/notice/list");
+ String detailUrl = environment.getProperty("bidding-api.detail-url",
+ "https://zbpublic.guojustar.com/#/detail/");
+ if (projectNo == null || projectNo.trim().isEmpty())
+ {
+ return error("项目编号不能为空");
+ }
+ try
+ {
+ String param = "pageNum=1&pageSize=200&keyword=" + URLEncoder.encode(projectNo.trim(), "UTF-8")
+ + "&projectType=10¬iceType=10";
+ String resp = HttpUtils.sendGet(listUrl, param);
+ if (resp == null || resp.isEmpty())
+ {
+ return error("招标公告查询失败");
+ }
+ JsonNode root = objectMapper.readTree(resp);
+ JsonNode rows = root.path("rows");
+ if (!rows.isArray() || rows.size() == 0)
+ {
+ return error("未查到该项目的招标公告");
+ }
+ long noticeId = rows.get(0).path("noticeId").asLong(0L);
+ if (noticeId == 0L)
+ {
+ return error("招标公告 noticeId 缺失");
+ }
+ // 注意: 不能用 success(url) —— BaseController.success(String) 会把 URL 当 msg、data 为 null
+ // 用 Map 承载, 前端取 data.detailUrl
+ Map data = new HashMap<>();
+ data.put("detailUrl", detailUrl + noticeId);
+ return success(data);
+ }
+ catch (Exception e)
+ {
+ log.error("招标公告查询失败, projectNo={}", projectNo, e);
+ return error("招标公告查询异常");
+ }
+ }
+
@GetMapping("/supportLetter/{annId}")
public AjaxResult supportLetterDetail(@PathVariable String annId) {
return success(supportLetterService.getById(annId));
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingInvoice.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingInvoice.java
index 2a2ad3c..8c5cca7 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingInvoice.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/BizMeetingInvoice.java
@@ -58,6 +58,18 @@ public class BizMeetingInvoice {
/** 原始文件名 (zip 解压时记录, 单文件路径可空) */
private String sourceFilename;
+ /** 所属材料类型 sub_type (join biz_meeting_material 带出, 发票表展示用) */
+ private String materialSubType;
+
+ /** 所属材料原始文件名 (join biz_meeting_material 带出, 单文件发票 source_filename 为空时兜底) */
+ private String materialFileName;
+
+ /** 发票号码 (OCR 识别) */
+ private String invoiceNo;
+
+ /** 开票日期 (OCR 识别, YYYY-MM-DD) */
+ private String invoiceDate;
+
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@@ -93,4 +105,16 @@ public class BizMeetingInvoice {
public String getSourceFilename() { return sourceFilename; }
public void setSourceFilename(String sourceFilename) { this.sourceFilename = sourceFilename; }
+
+ public String getMaterialSubType() { return materialSubType; }
+ public void setMaterialSubType(String materialSubType) { this.materialSubType = materialSubType; }
+
+ public String getMaterialFileName() { return materialFileName; }
+ public void setMaterialFileName(String materialFileName) { this.materialFileName = materialFileName; }
+
+ public String getInvoiceNo() { return invoiceNo; }
+ public void setInvoiceNo(String invoiceNo) { this.invoiceNo = invoiceNo; }
+
+ public String getInvoiceDate() { return invoiceDate; }
+ public void setInvoiceDate(String invoiceDate) { this.invoiceDate = invoiceDate; }
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
index c436ed1..ee13ec8 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/domain/vo/BizMeetingAttendeeImportVo.java
@@ -93,8 +93,8 @@ public class BizMeetingAttendeeImportVo {
@Excel(name = "银行详细地址", sort = 18)
private String bankAddress;
- /** 身份证附件(正反面 URL CSV) */
- @Excel(name = "身份证附件", sort = 19)
+ /** 身份证附件(正反面 URL CSV)— 仅导出,不进入导入模板(附件文件无法通过 Excel 导入) */
+ @Excel(name = "身份证附件", sort = 19, type = Excel.Type.EXPORT)
private String idCardAttachments;
public String getPhone() { return phone; }
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingInvoiceMapper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingInvoiceMapper.java
index b5502bf..e0316c2 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingInvoiceMapper.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/mapper/BizMeetingInvoiceMapper.java
@@ -51,6 +51,22 @@ public interface BizMeetingInvoiceMapper {
int updateAmountByMaterial(@org.apache.ibatis.annotations.Param("materialId") Long materialId,
@org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount);
+ /**
+ * 单文件 OCR 完成: 更新金额 + 发票号 + 开票日期 (按 material_id, 与状态更新分开调用)
+ */
+ int updateOcrResultByMaterial(@org.apache.ibatis.annotations.Param("materialId") Long materialId,
+ @org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount,
+ @org.apache.ibatis.annotations.Param("invoiceNo") String invoiceNo,
+ @org.apache.ibatis.annotations.Param("invoiceDate") String invoiceDate);
+
+ /**
+ * 兜底 OCR 完成: 更新状态(RECOGNIZED) + 金额 + 发票号 + 开票日期 (按主键)
+ */
+ int updateOcrResultByPrimaryKey(@org.apache.ibatis.annotations.Param("id") Long id,
+ @org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount,
+ @org.apache.ibatis.annotations.Param("invoiceNo") String invoiceNo,
+ @org.apache.ibatis.annotations.Param("invoiceDate") String invoiceDate);
+
/**
* 兜底 OCR: 按主键更新 (状态 + 金额 + 错误信息)
*/
@@ -58,4 +74,10 @@ public interface BizMeetingInvoiceMapper {
@org.apache.ibatis.annotations.Param("recognizeStatus") String recognizeStatus,
@org.apache.ibatis.annotations.Param("amount") java.math.BigDecimal amount,
@org.apache.ibatis.annotations.Param("errorMsg") String errorMsg);
+
+ /**
+ * 材料全删全插后, 未变材料的发票 material_id 需从旧 id 迁移到新 id (否则发票表 join 不到材料类型)
+ */
+ int updateInvoiceMaterialId(@org.apache.ibatis.annotations.Param("oldMaterialId") Long oldMaterialId,
+ @org.apache.ibatis.annotations.Param("newMaterialId") Long newMaterialId);
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/ZipExtractor.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/ZipExtractor.java
index 6ddaba0..009d87d 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/ZipExtractor.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/ocr/ZipExtractor.java
@@ -7,6 +7,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
@@ -39,31 +41,17 @@ public class ZipExtractor
if (size <= 0) throw new IOException("OSS zip 下载失败: " + zipUrl);
log.info("zip 下载: url={} size={}B tmp={}", zipUrl, size, zipFile.getAbsolutePath());
- List out = new ArrayList<>();
- try (ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile)))
+ // 优先 UTF-8 解包; 若 entry 名非 UTF-8 (常见 GBK, Windows 打包) 会抛
+ // IllegalArgumentException (malformed input) → 回退 GBK 重试
+ try
{
- ZipEntry e;
- while ((e = zin.getNextEntry()) != null)
- {
- if (e.isDirectory()) continue;
- String name = e.getName();
- String lower = name.toLowerCase();
- if (!(lower.endsWith(".png") || lower.endsWith(".jpg")
- || lower.endsWith(".jpeg") || lower.endsWith(".pdf")))
- {
- continue;
- }
- // 子目录展平: a/b/c.png → tmpRoot/a_b_c.png
- File outFile = new File(tmpRoot, name.replace("/", "_"));
- try (FileOutputStream fos = new FileOutputStream(outFile))
- {
- zin.transferTo(fos);
- }
- out.add(outFile);
- }
+ return doExtract(zipFile, tmpRoot, StandardCharsets.UTF_8);
+ }
+ catch (IllegalArgumentException e)
+ {
+ log.warn("zip 文件名非 UTF-8, 回退 GBK 重试: {}", e.getMessage());
+ return doExtract(zipFile, tmpRoot, Charset.forName("GBK"));
}
- log.info("zip 解压完成: 文件数={}", out.size());
- return out;
}
finally
{
@@ -72,6 +60,38 @@ public class ZipExtractor
}
}
+ /**
+ * 按指定 charset 解压 zip, 返回图片/PDF 文件列表
+ */
+ private static List doExtract(File zipFile, File tmpRoot, Charset charset) throws IOException
+ {
+ List out = new ArrayList<>();
+ try (ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile), charset))
+ {
+ ZipEntry e;
+ while ((e = zin.getNextEntry()) != null)
+ {
+ if (e.isDirectory()) continue;
+ String name = e.getName();
+ String lower = name.toLowerCase();
+ if (!(lower.endsWith(".png") || lower.endsWith(".jpg")
+ || lower.endsWith(".jpeg") || lower.endsWith(".pdf")))
+ {
+ continue;
+ }
+ // 子目录展平: a/b/c.png → tmpRoot/a_b_c.png
+ File outFile = new File(tmpRoot, name.replace("/", "_"));
+ try (FileOutputStream fos = new FileOutputStream(outFile))
+ {
+ zin.transferTo(fos);
+ }
+ out.add(outFile);
+ }
+ }
+ log.info("zip 解压完成: charset={} 文件数={}", charset, out.size());
+ return out;
+ }
+
/** 清理整个解压临时目录 */
public static void cleanup(File tmpRoot)
{
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/pdf/SealStamper.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/pdf/SealStamper.java
new file mode 100644
index 0000000..7d75b11
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/pdf/SealStamper.java
@@ -0,0 +1,84 @@
+package com.ruoyi.business.pdf;
+
+import com.itextpdf.io.image.ImageDataFactory;
+import com.itextpdf.kernel.events.Event;
+import com.itextpdf.kernel.events.IEventHandler;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.Canvas;
+import com.itextpdf.layout.element.Image;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 劳务协议盖章 (固定定位)
+ *
+ * 在指定页 (默认第 2 页) 固定坐标叠加公章: x 固定, y = 页面垂直中心偏上 yOffset.
+ * 使用 {@link PdfDocumentEvent#END_PAGE} 事件叠加在页面内容流之后, 公章 z 序在正文之上.
+ *
+ *
容错: 印章图片缺失 / 加载失败时只记日志并跳过, 不阻断 PDF 生成.
+ */
+public class SealStamper implements IEventHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(SealStamper.class);
+
+ /** 印章图片相对路径 (运行目录, 与 simsun.ttc 同级) */
+ private final String sealImagePath;
+ /** 印章水平坐标 (iText 坐标 x 从左往右) */
+ private final float x;
+ /** 印章垂直: 页面垂直中心偏上的偏移量 (正=往上) */
+ private final float yOffsetFromCenter;
+ /** 印章尺寸 (宽高, px) */
+ private final float width;
+ private final float height;
+ /** 盖章页码 (从 1 起) */
+ private final int pageNumber;
+
+ public SealStamper(String sealImagePath, float x, float yOffsetFromCenter,
+ float width, float height, int pageNumber) {
+ this.sealImagePath = sealImagePath;
+ this.x = x;
+ this.yOffsetFromCenter = yOffsetFromCenter;
+ this.width = width;
+ this.height = height;
+ this.pageNumber = pageNumber;
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
+ PdfDocument document = documentEvent.getDocument();
+ int n = document.getPageNumber(documentEvent.getPage());
+ // 只盖指定页
+ if (n != pageNumber) {
+ return;
+ }
+
+ Image image;
+ try {
+ image = new Image(ImageDataFactory.create(sealImagePath));
+ } catch (Exception e) {
+ log.warn("[seal] 印章图片加载失败, 跳过盖章: {}", sealImagePath, e);
+ return;
+ }
+ image.setWidth(width);
+ image.setHeight(height);
+
+ PdfPage page = documentEvent.getPage();
+ Rectangle pageSize = page.getPageSize();
+ // y = 页面垂直中心偏上 yOffsetFromCenter (iText 坐标 y 从下往上)
+ float y = pageSize.getHeight() / 2 + yOffsetFromCenter;
+ image.setFixedPosition(x, y);
+
+ PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), document);
+ Canvas canvas = new Canvas(pdfCanvas, pageSize);
+ try {
+ canvas.add(image);
+ } finally {
+ canvas.close();
+ }
+ }
+}
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
index e40e4a9..1a54d01 100644
--- 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
@@ -49,7 +49,7 @@ public class SupplierAccountPullScheduler
private static final String FULL_PULL_DAYS = "supplier-account-api.full-pull-days";
private static final String DEFAULT_BASE_URL =
- "https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts";
+ "https://zbsupplier.guojustar.com/supplier-api/bidding/supplier/openapi/accounts";
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
index 5a41683..d3382e1 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/PdfService.java
@@ -3,6 +3,7 @@ package com.ruoyi.business.service;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.io.font.PdfEncodings;
+import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
@@ -10,6 +11,7 @@ import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.font.FontProvider;
import com.ruoyi.business.oss.OssUploader;
+import com.ruoyi.business.pdf.SealStamper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -28,6 +30,18 @@ public class PdfService {
@Autowired
private OssUploader ossUploader;
+ /** 公章图片相对路径 (运行目录, 与 simsun.ttc 同级) */
+ private static final String SEAL_IMAGE_PATH = "zhenghe.png";
+ /** 公章水平坐标 (保持不动) */
+ private static final float SEAL_X = 80f;
+ /** 公章垂直: 页面正中间偏上 15 */
+ private static final float SEAL_Y_OFFSET = 15f;
+ /** 公章尺寸 (宽高, px) */
+ private static final float SEAL_WIDTH = 120f;
+ private static final float SEAL_HEIGHT = 120f;
+ /** 盖章页码 (第 2 页) */
+ private static final int SEAL_PAGE = 2;
+
/**
* HTML 字符串 → PDF 字节流
*/
@@ -48,6 +62,10 @@ public class PdfService {
fontProvider.addFont(cjkFont.getFontProgram(), PdfEncodings.IDENTITY_H);
properties.setFontProvider(fontProvider);
+ // 盖章: 第 2 页固定坐标叠加公章 (END_PAGE 事件, 必须在 convertToPdf 之前注册, 否则 document 已关闭)
+ pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE,
+ new SealStamper(SEAL_IMAGE_PATH, SEAL_X, SEAL_Y_OFFSET, SEAL_WIDTH, SEAL_HEIGHT, SEAL_PAGE));
+
HtmlConverter.convertToPdf(fullHtml, pdfDocument, properties);
pdfDocument.close();
return baos.toByteArray();
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java
index 205f792..8ee1af9 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/StageDeriver.java
@@ -119,7 +119,7 @@ public class StageDeriver
}
// 仅 待审核/审核通过/待整改/待结算 四类加前缀
- if ("待审核".equals(label) || "审核通过".equals(label) || "待整改".equals(label) || "待结算".equals(label))
+ if ("待审核".equals(label) || "审核通过".equals(label) || "待整改".equals(label) || "待结算".equals(label) || "待监察".equals(label))
{
return ("labor".equals(track) ? "劳务" : "会务") + label;
}
@@ -179,11 +179,11 @@ public class StageDeriver
if (phase == 1) return "执行中";
return "已执行";
case 2: // C0 合规审中
- if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
- return "待审核"; // executor / manager / admin
+ return "待审核"; // executor / manager / sponsor / admin
case 3: // C1 支持方审中
if ("manager".equals(role)) return "审核通过";
- return "待审核"; // executor / sponsor / admin
+ if ("sponsor".equals(role)) return "待监察";
+ return "待审核"; // executor / admin
default: // 4 = A 已通过
return "待结算";
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java
index f320d28..87897d6 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizAdminUserServiceImpl.java
@@ -26,7 +26,7 @@ import com.ruoyi.system.service.ISysUserService;
@Service
public class BizAdminUserServiceImpl implements IBizAdminUserService {
- private static final Set ROLE_WHITELIST = Set.of("admin", "manager", "doctor", "executor", "sponsor");
+ private static final Set ROLE_WHITELIST = Set.of("admin", "manager", "doctor", "executor", "sponsor", "leader");
@Autowired
private ISysUserService sysUserService;
@@ -79,6 +79,7 @@ public class BizAdminUserServiceImpl implements IBizAdminUserService {
switch (role) {
case "admin":
case "manager":
+ case "leader": // 项目负责人: 纯 sys_user (姓名落 sys_user.nick_name, 供 lead_user_name 反查兜底)
return insertPlain(b, role, encPwd);
case "doctor":
return insertDoctor(b, encPwd);
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
index b1a0ed9..9555cb0 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizMeetingMaterialServiceImpl.java
@@ -37,6 +37,7 @@ import com.ruoyi.business.domain.BizMeeting;
import com.ruoyi.business.domain.BizMeetingAttendee;
import com.ruoyi.business.domain.BizMeetingMaterial;
import com.ruoyi.business.mapper.BizMeetingMapper;
+import com.ruoyi.business.mapper.BizMeetingInvoiceMapper;
import com.ruoyi.business.mapper.BizMeetingMaterialMapper;
import com.ruoyi.business.oss.OssUploader;
import com.ruoyi.business.oss.OssZipService;
@@ -54,8 +55,8 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
private static final Set CAMERA_SUBTYPES = new HashSet<>(Arrays.asList(
"L_SIGN_IN", "L_PANORAMA_FRONT", "L_PANORAMA_BACK"));
- /** 可识别文件扩展名 (与前端 isRecognizable 对齐): 图片/PDF 才触发 OCR */
- private static final Pattern RECOGNIZABLE = Pattern.compile("\\.(jpe?g|png|pdf)$", Pattern.CASE_INSENSITIVE);
+ /** 可识别文件扩展名 (与前端 isRecognizable 对齐): 图片/PDF/zip 才触发 OCR (zip 走解压分析发票) */
+ private static final Pattern RECOGNIZABLE = Pattern.compile("\\.(jpe?g|png|pdf|zip)$", Pattern.CASE_INSENSITIVE);
/** 会务下载范围: 会务材料 (SERVICE) + 会务凭证 (SERVICE_VOUCHER) */
private static final Set SERVICE_MATERIAL_TYPES = new HashSet<>(Arrays.asList("SERVICE", "SERVICE_VOUCHER"));
@@ -114,6 +115,8 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
@Autowired
private BizMeetingMapper bizMeetingMapper;
@Autowired
+ private BizMeetingInvoiceMapper bizMeetingInvoiceMapper;
+ @Autowired
private OssZipService ossZipService;
@Autowired
private IBizMeetingAttendeeService attendeeService;
@@ -162,11 +165,28 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
}
}
+ // 被移除的 subType (旧有、新无) 的发票一并删除, 否则发票表残留孤儿发票行 (material_id 指向已删材料)
+ Set newSubTypes = new HashSet<>();
+ if (list != null) {
+ for (BizMeetingMaterial m : list) {
+ if (m.getSubType() != null) newSubTypes.add(m.getSubType());
+ }
+ }
+ if (oldList != null) {
+ for (BizMeetingMaterial o : oldList) {
+ if (o.getSubType() != null && o.getId() != null && !newSubTypes.contains(o.getSubType())) {
+ bizMeetingInvoiceMapper.deleteByMaterialId(o.getId());
+ }
+ }
+ }
+
bizMeetingMaterialMapper.deleteByMeetingId(meetingId);
if (list == null || list.isEmpty()) {
return list;
}
// 不接受前端传的 id, 走 AUTO_INCREMENT; meetingId 兜底由路径提供
+ // 未变材料的旧 id (subType -> 旧 material id), 全删全插后迁移其发票 material_id 到新 id
+ Map unchangedOldId = new HashMap<>();
for (BizMeetingMaterial m : list) {
m.setId(null);
m.setMeetingId(meetingId);
@@ -177,6 +197,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
m.setAmount(old.getAmount());
m.setFeeStatus(1);
m.setExtraOssUrl(old.getExtraOssUrl());
+ unchangedOldId.put(m.getSubType(), old.getId());
} else {
// 新增/替换: 金额清零, 会 OCR 的发票标记待计算
m.setAmount(null);
@@ -199,6 +220,11 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
for (BizMeetingMaterial m : list) {
if (m.getSubType() != null) {
m.setId(idBySubType.get(m.getSubType()));
+ // 未变材料: 材料全删全插导致 id 变化, 迁移其发票 material_id 旧 id → 新 id
+ Long oldId = unchangedOldId.get(m.getSubType());
+ if (oldId != null && m.getId() != null && !m.getId().equals(oldId)) {
+ bizMeetingInvoiceMapper.updateInvoiceMaterialId(oldId, m.getId());
+ }
}
}
return list;
@@ -500,10 +526,74 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
byte[] excel = attendeeService.buildAttendeeInfoExcel(attendees);
ossZipService.putObject(prefix + folderName + "/参会人信息/参会人信息.xlsx", excel, XLSX_CONTENT_TYPE);
copied++;
+ // 补参会人照片/身份证照/劳务协议 (存 attendee 表, 材料表扫不到, 单独 copy 进劳务 zip)
+ copied += stageAttendeeFiles(attendees, prefix, folderName);
}
return copied;
}
+ /** 把参会人照片(on_site_photos)/身份证照(id_card_attachments)/劳务协议(labor_protocol) copy 进劳务 zip, 返回写入文件数 */
+ private int stageAttendeeFiles(List attendees, String prefix, String folderName)
+ {
+ int copied = 0;
+ for (int i = 0; i < attendees.size(); i++)
+ {
+ BizMeetingAttendee a = attendees.get(i);
+ // 序号 = 列表 1 起下标, 与前端表格 index 及"下载目录模板"的 {序号}_{姓名} 命名对齐
+ String person = (i + 1) + "_" + safeName(a.getName());
+ copied += copyCsvUrls(a.getOnSitePhotos(), prefix + folderName + "/参会人照片/" + person + "/", "照片");
+ copied += copyIdCardFiles(a.getIdCardAttachments(), prefix + folderName + "/参会人身份证/" + person + "/");
+ copied += copySingleUrl(a.getLaborProtocol(), prefix + folderName + "/劳务协议/", person);
+ }
+ return copied;
+ }
+
+ /** 逗号分隔 OSS URL → 逐个 copy 到 dstDir, 文件名 = baseName-i{ext}; 返回 copy 数 */
+ private int copyCsvUrls(String csvUrls, String dstDir, String baseName)
+ {
+ if (csvUrls == null || csvUrls.trim().isEmpty()) return 0;
+ String[] urls = csvUrls.split(",");
+ int copied = 0;
+ for (int i = 0; i < urls.length; i++)
+ {
+ String srcKey = ossZipService.extractKey(urls[i].trim());
+ if (srcKey == null || srcKey.isEmpty()) continue;
+ String fileName = safeName(baseName) + "-" + (i + 1) + extOf(srcKey);
+ ossZipService.copyObject(srcKey, dstDir + fileName);
+ copied++;
+ }
+ return copied;
+ }
+
+ /** 身份证照 (正/反两张, 顺序固定 front,back) copy 到 dstDir, 文件名 身份证-正面/身份证-反面{ext}; 返回 copy 数 */
+ private int copyIdCardFiles(String csvUrls, String dstDir)
+ {
+ if (csvUrls == null || csvUrls.trim().isEmpty()) return 0;
+ String[] urls = csvUrls.split(",");
+ String[] labels = { "身份证-正面", "身份证-反面" };
+ int copied = 0;
+ for (int i = 0; i < urls.length && i < labels.length; i++)
+ {
+ String srcKey = ossZipService.extractKey(urls[i].trim());
+ if (srcKey == null || srcKey.isEmpty()) continue;
+ String fileName = labels[i] + extOf(srcKey);
+ ossZipService.copyObject(srcKey, dstDir + fileName);
+ copied++;
+ }
+ return copied;
+ }
+
+ /** 单个 OSS URL → copy 到 dstDir, 文件名 = baseName{ext}; 返回 copy 数 (0/1) */
+ private int copySingleUrl(String url, String dstDir, String baseName)
+ {
+ if (url == null || url.trim().isEmpty()) return 0;
+ String srcKey = ossZipService.extractKey(url.trim());
+ if (srcKey == null || srcKey.isEmpty()) return 0;
+ String fileName = safeName(baseName) + extOf(srcKey);
+ ossZipService.copyObject(srcKey, dstDir + fileName);
+ return 1;
+ }
+
/** 把会议日程海报 (biz_meeting.schedule_url) staging 到 zip 的"日程海报"目录, 返回写入的文件数 (0 = 无日程海报) */
private int stageSchedulePoster(BizMeeting meeting, String prefix, String folderName)
{
@@ -732,7 +822,7 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
}
// 与前端"保存"流程一致, 后台触发 OCR (识别为发票回写 amount → 会议费用汇总)
if (ocrNeeded) {
- invoiceOcrService.submitRecognition(materialId, meetingId, ossUrl, isZip, oldMaterialId);
+ invoiceOcrService.submitRecognition(materialId, meetingId, ossUrl, oldMaterialId);
}
updated++;
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
index 62fc987..fb3e959 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/BizSignServiceImpl.java
@@ -329,7 +329,7 @@ public class BizSignServiceImpl implements BizSignService {
throw new ServiceException("手写签名不能为空");
}
String fullPdfUrl = pdfService.htmlToPdfFile(contentHtml, "labor/" + attendee.getMeetingId());
- String maskedHtml = desensitizeHtml(contentHtml);
+ String maskedHtml = desensitizeHtml(contentHtml, attendee);
String maskedPdfUrl = pdfService.htmlToPdfFile(maskedHtml, "labor/" + attendee.getMeetingId() + "/masked");
BizMeetingAttendee update = new BizMeetingAttendee();
update.setId(attendeeId);
@@ -346,7 +346,7 @@ public class BizSignServiceImpl implements BizSignService {
return result;
}
- private String desensitizeHtml(String html) {
+ private String desensitizeHtml(String html, BizMeetingAttendee attendee) {
if (html == null) return null;
java.util.regex.Pattern idCardP = java.util.regex.Pattern.compile("(?
",
+ "");
+ }
+ }
+
+ // 姓名/账户名称: 中文文本无法按数字正则定位, 用已知原值精确替换 (长串优先, 避免姓名是账户名的子串时漏脱敏)
+ if (attendee != null) {
+ java.util.List texts = new java.util.ArrayList<>();
+ if (attendee.getName() != null && !attendee.getName().trim().isEmpty()) texts.add(attendee.getName().trim());
+ if (attendee.getAccountName() != null && !attendee.getAccountName().trim().isEmpty()) texts.add(attendee.getAccountName().trim());
+ texts.sort((a, b) -> Integer.compare(b.length(), a.length()));
+ for (String t : texts) {
+ if (t.length() < 2) continue; // 单字姓名/账户名跳过, 避免误替换正文中的同字
+ String masked = maskName(t);
+ if (!masked.equals(t)) {
+ html = html.replace(t, masked);
+ }
+ }
+ }
+ return html;
+ }
+
+ /** 姓名脱敏: 保留首字, 其余打码 (张三 → 张*) */
+ private String maskName(String v) {
+ if (v == null) return null;
+ String s = v.trim();
+ if (s.isEmpty()) return v;
+ if (s.length() <= 1) return "*";
+ return s.charAt(0) + "*".repeat(s.length() - 1);
}
}
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/InvoiceOcrService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/InvoiceOcrService.java
index 7931abc..c98a4f1 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/InvoiceOcrService.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/impl/InvoiceOcrService.java
@@ -74,13 +74,13 @@ public class InvoiceOcrService
* @param materialId biz_meeting_material.id
* @param meetingId 会议 ID
* @param ossUrl OSS URL (单文件: 原图;ZIP: zip 包)
- * @param isZip true → ZIP 路径;false → 单文件路径
* @param oldMaterialId 替换场景携带, 后端先 DELETE invoice WHERE material_id=old + material.amount=0;
* null → 新增场景, 不做清理
* @return 提交摘要 (立即返回, 不等 OCR 完成)
+ * 是否 zip 不再由前端传, 后端按 ossUrl 后缀 (转小写) 判断, 避免前后端不一致.
*/
public RecognizeResult submitRecognition(Long materialId, Long meetingId, String ossUrl,
- boolean isZip, Long oldMaterialId)
+ Long oldMaterialId)
{
RecognizeResult out = new RecognizeResult();
if (materialId == null || meetingId == null || ossUrl == null || ossUrl.isEmpty())
@@ -89,6 +89,11 @@ public class InvoiceOcrService
out.setErrorMsg("参数缺失");
return out;
}
+ // 是否 zip: 按 ossUrl 后缀 (转小写) 判断, 不信任前端传参
+ boolean isZip = ossUrl.toLowerCase().endsWith(".zip");
+
+ // 提前取登录用户 ID: OCR 在后台线程池跑, 无 SecurityContext, 必须在此 (HTTP 线程) 取好
+ final Long creatorId = SecurityUtils.getUserId();
// 1. 替换场景: 先清旧 (deleteByMaterialId + amount=0)
if (oldMaterialId != null)
@@ -107,7 +112,7 @@ public class InvoiceOcrService
placeholder.setOssUrl(ossUrl);
placeholder.setInvoiceType("SUB");
placeholder.setRecognizeStatus("UNRECOGNIZED");
- placeholder.setCreatorId(SecurityUtils.getUserId());
+ placeholder.setCreatorId(creatorId);
placeholder.setCreateTime(new Date());
placeholder.setUpdateTime(new Date());
invoiceMapper.insert(placeholder);
@@ -121,7 +126,7 @@ public class InvoiceOcrService
{
if (isZip)
{
- recognizeZip(materialId, meetingId, url);
+ recognizeZip(materialId, meetingId, url, creatorId);
}
else
{
@@ -172,7 +177,8 @@ public class InvoiceOcrService
? BigDecimal.valueOf(ir.getFields().getAmount())
: BigDecimal.ZERO;
invoiceMapper.updateStatusByMaterial(materialId, "RECOGNIZED", null);
- invoiceMapper.updateAmountByMaterial(materialId, amount);
+ invoiceMapper.updateOcrResultByMaterial(materialId, amount,
+ ir.getFields().getInvoiceNo(), ir.getFields().getInvoiceDate());
materialService.updateAmount(materialId, amount);
log.info("单文件识别成功: materialId={} amount={} elapsed={}ms",
materialId, amount, ir.getElapsedMs());
@@ -196,7 +202,7 @@ public class InvoiceOcrService
*
* material 表只动一次 (末尾的 updateAmount), 即使 0 张发票也照写; 非发票文件不进任何统计
*/
- void recognizeZip(Long materialId, Long meetingId, String zipUrl) throws Exception
+ void recognizeZip(Long materialId, Long meetingId, String zipUrl, Long creatorId) throws Exception
{
File tmpRoot = null;
try
@@ -239,9 +245,11 @@ public class InvoiceOcrService
inv.setOssUrl(newUrl);
inv.setInvoiceType("SUB");
inv.setAmount(amount);
+ inv.setInvoiceNo(ir.getFields().getInvoiceNo());
+ inv.setInvoiceDate(ir.getFields().getInvoiceDate());
inv.setRecognizeStatus("RECOGNIZED");
inv.setSourceFilename(f.getName());
- inv.setCreatorId(SecurityUtils.getUserId());
+ inv.setCreatorId(creatorId);
inv.setCreateTime(new Date());
inv.setUpdateTime(new Date());
invoiceMapper.insert(inv);
@@ -287,7 +295,8 @@ public class InvoiceOcrService
BigDecimal amount = ir.getFields().getAmount() != null
? BigDecimal.valueOf(ir.getFields().getAmount())
: BigDecimal.ZERO;
- invoiceMapper.updateStatusAndAmountByPrimaryKey(inv.getId(), "RECOGNIZED", amount, null);
+ invoiceMapper.updateOcrResultByPrimaryKey(inv.getId(), amount,
+ ir.getFields().getInvoiceNo(), ir.getFields().getInvoiceDate());
materialService.updateAmount(inv.getMaterialId(), amount);
log.info("兜底识别成功: invoiceId={} amount={}", inv.getId(), amount);
}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java
index 8ff50ea..f077162 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/sms/AliyunSmsSender.java
@@ -45,7 +45,7 @@ public class AliyunSmsSender {
@Value("${ruoyi.sms.esignTemplate}")
private String esignTemplate;
- @Value("${ruoyi.sms.esignBaseUrl:https://risingdoctor.com/hg}")
+ @Value("${ruoyi.sms.esignBaseUrl:https://hegui.bahim.org.cn}")
private String esignBaseUrl;
@Value("${ruoyi.sms.regionId:cn-hangzhou}")
diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingInvoiceMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingInvoiceMapper.xml
index 075b186..88c961b 100644
--- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingInvoiceMapper.xml
+++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingInvoiceMapper.xml
@@ -15,11 +15,15 @@
+
+
+
+
select id, meeting_id, material_id, oss_url, invoice_type, amount, creator_id, create_time, update_time,
- recognize_status, error_msg, source_filename
+ recognize_status, error_msg, source_filename, invoice_no, invoice_date
from biz_meeting_invoice
@@ -45,9 +49,13 @@
@@ -64,6 +72,8 @@
recognize_status,
error_msg,
source_filename,
+ invoice_no,
+ invoice_date,
#{meetingId},
@@ -77,6 +87,8 @@
#{recognizeStatus},
#{errorMsg},
#{sourceFilename},
+ #{invoiceNo},
+ #{invoiceDate},
@@ -150,4 +162,33 @@
where id = #{id}
+
+
+ update biz_meeting_invoice
+ set amount = #{amount},
+ invoice_no = #{invoiceNo},
+ invoice_date = #{invoiceDate},
+ update_time = now()
+ where material_id = #{materialId}
+
+
+
+
+ update biz_meeting_invoice
+ set recognize_status = 'RECOGNIZED',
+ amount = #{amount},
+ invoice_no = #{invoiceNo},
+ invoice_date = #{invoiceDate},
+ update_time = now()
+ where id = #{id}
+
+
+
+
+ update biz_meeting_invoice
+ set material_id = #{newMaterialId},
+ update_time = now()
+ where material_id = #{oldMaterialId}
+
+
\ No newline at end of file
diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml
index 5e531ad..6fc7eae 100644
--- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml
+++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizMeetingMapper.xml
@@ -114,6 +114,9 @@
and project_id in (
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
)
+ and project_id in (
+ select project_id from biz_project where lead_user_id = #{params.leaderUserId} and is_deleted = 0
+ )
order by meeting_id desc
@@ -134,6 +137,7 @@
and p2.is_finished = '1' and p2.open_status = 'N'
)
+ and project_id in (select project_id from biz_project where lead_user_id = #{params.leaderUserId} and is_deleted = 0)
group by current_stage
diff --git a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml
index 3df9052..34a8766 100644
--- a/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml
+++ b/ry-api/ruoyi-business/src/main/resources/mapper/business/BizProjectMapper.xml
@@ -362,6 +362,7 @@
p.is_deleted = 0
and create_user_id = #{params.createUserId}
+ and p.lead_user_id = #{params.leaderUserId}
and p.is_published = #{isPublished}
and project_id in
diff --git a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
index 534d21c..c6c6771 100644
--- a/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
+++ b/ry-api/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
@@ -73,6 +73,9 @@ public class HttpUtils
log.info("sendGet - {}", urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
+ // 连接/读超时: 防止外部接口不可达时无限挂起 (后台子任务会占死单线程调度器)
+ connection.setConnectTimeout(10000);
+ connection.setReadTimeout(30000);
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
index ea33312..837b069 100644
--- a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
+++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -16,6 +16,7 @@ import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
+import com.ruoyi.framework.security.filter.LeaderReadOnlyFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
@@ -80,6 +81,8 @@ public class SecurityConfig
})
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
+ // leader 只读拦截: 在 JWT 鉴权之后 (SecurityContext 已就绪), 任何业务写操作前拦下
+ .addFilterAfter(new LeaderReadOnlyFilter(), JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class)
.build();
diff --git a/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/LeaderReadOnlyFilter.java b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/LeaderReadOnlyFilter.java
new file mode 100644
index 0000000..c53359a
--- /dev/null
+++ b/ry-api/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/LeaderReadOnlyFilter.java
@@ -0,0 +1,84 @@
+package com.ruoyi.framework.security.filter;
+
+import java.io.IOException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.OncePerRequestFilter;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 「项目负责人」(leader) 角色只读拦截.
+ *
+ * leader 是纯查看角色: 除了查看自己的资料/改密码 (账号信息页), 不允许任何业务写操作.
+ * 后端兜底 (前端隐藏按钮之外, 防 devtools 直接调接口):
+ *
+ * - GET 请求一律放行.
+ * - 非 GET (POST/PUT/DELETE) 仅放行本人资料维护: {@code /system/user/profile} 与
+ * {@code /system/user/profile/updatePwd} (SysProfileController 的 GET/PUT 与改密码).
+ * - 其余非 GET 一律 403.
+ *
+ * 与 JwtAuthenticationTokenFilter 之后注册 (SecurityConfig), 依赖其已写入 SecurityContext.
+ */
+public class LeaderReadOnlyFilter extends OncePerRequestFilter
+{
+ /** 放行的本人资料维护路径 (账号信息页) */
+ private static final String[] SELF_SERVICE_ALLOWED = {
+ "/system/user/profile",
+ "/system/user/profile/updatePwd",
+ };
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException
+ {
+ if (HttpMethod.GET.matches(request.getMethod())
+ || !isLeader()
+ || isSelfService(request.getRequestURI()))
+ {
+ chain.doFilter(request, response);
+ return;
+ }
+ // leader 非 GET 且非本人资料维护 → 只读拒绝
+ response.setStatus(HttpStatus.FORBIDDEN);
+ ServletUtils.renderString(response, JSON.toJSONString(
+ AjaxResult.error(HttpStatus.FORBIDDEN, "项目负责人无操作权限")));
+ }
+
+ /** 当前登录人是否为 leader (role_type 单一可信源), 未登录/异常视为非 leader (放行交给后续鉴权) */
+ private boolean isLeader()
+ {
+ try
+ {
+ return "leader".equals(SecurityUtils.getLoginUser().getUser().getRoleType());
+ }
+ catch (Exception e)
+ {
+ return false;
+ }
+ }
+
+ /** 是否命中本人资料维护白名单 (账号信息页: 查看/修改资料 + 改密码) */
+ private boolean isSelfService(String uri)
+ {
+ if (StringUtils.isEmpty(uri))
+ {
+ return false;
+ }
+ for (String path : SELF_SERVICE_ALLOWED)
+ {
+ if (uri.equals(path) || uri.startsWith(path + "/"))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/ry-api/zhenghe.png b/ry-api/zhenghe.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f3087d507c29bd64ca47a54ad1c9387b8885e39
GIT binary patch
literal 19191
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
literal 0
HcmV?d00001
diff --git a/ry-h5/pages/signin/index.uvue b/ry-h5/pages/signin/index.uvue
index 3eee089..2c0905c 100644
--- a/ry-h5/pages/signin/index.uvue
+++ b/ry-h5/pages/signin/index.uvue
@@ -66,9 +66,9 @@
import { useCamera } from '@/composables/useCamera'
const videoId = 'signin-video'
-// A4 纸: 从顶边到底边的一竖条, 水平位置在 70%~85% 宽度 (签到人手机号/身份证号所在列) 做高斯模糊脱敏
+// A4 纸: 从顶边到底边的一竖条, 水平位置在 60%~70% 宽度 (签到人手机号/身份证号所在列) 做高斯模糊脱敏
const { captured, errorMsg, startCamera, flipCamera, takePhoto, retake, confirm } =
- useCamera(videoId, { start: 0.7, end: 0.85, radius: 20 })
+ useCamera(videoId, { start: 0.6, end: 0.7, radius: 20 })
function goBack() {
uni.navigateBack()
diff --git a/ry-vue3/.env.production b/ry-vue3/.env.production
index b2b9dbd..2325198 100644
--- a/ry-vue3/.env.production
+++ b/ry-vue3/.env.production
@@ -1,5 +1,5 @@
# 生产环境 API 基路径 (nginx 反向代理前缀)
VITE_APP_BASE_API = '/hg-api'
-# 生产环境静态资源子路径 (nginx 部署在 /hg/)
-VITE_BASE = '/hg/'
+# 生产环境静态资源子路径 (nginx 部署在根路径 /)
+VITE_BASE = '/'
diff --git a/ry-vue3/.env.test b/ry-vue3/.env.test
new file mode 100644
index 0000000..eafed6e
--- /dev/null
+++ b/ry-vue3/.env.test
@@ -0,0 +1,5 @@
+# 测试环境 API 基路径 (nginx 反向代理前缀)
+VITE_APP_BASE_API = '/hegui-api'
+
+# 测试环境静态资源子路径 (nginx 部署在 /hegui/)
+VITE_BASE = '/hegui/'
diff --git a/ry-vue3/package.json b/ry-vue3/package.json
index 9198359..2d96b67 100644
--- a/ry-vue3/package.json
+++ b/ry-vue3/package.json
@@ -4,7 +4,9 @@
"private": true,
"scripts": {
"dev": "vite",
- "build": "vite build",
+ "build": "vite build --mode production",
+ "build:prod": "vite build --mode production",
+ "build:test": "vite build --mode test",
"preview": "vite preview"
},
"dependencies": {
diff --git a/ry-vue3/src/api/system.js b/ry-vue3/src/api/system.js
index 9d95cf4..32b043c 100644
--- a/ry-vue3/src/api/system.js
+++ b/ry-vue3/src/api/system.js
@@ -20,8 +20,8 @@ export function listByRole(query) {
// 兼容旧名 (项目分配弹窗已不再使用, 保留避免破坏其它 import)
export const listExecutor = (query) => listByRole({ roleType: 'executor', ...query })
export const listSupporters = (query) => listByRole({ roleType: 'sponsor', ...query })
-// 合规管理员 (sys_user.role_type='manager', 选项目负责人用)
-export const listManagers = (query) => listByRole({ roleType: 'manager', ...query })
+// 项目负责人 (sys_user.role_type='leader', 选项目负责人用)
+export const listLeaders = (query) => listByRole({ roleType: 'leader', ...query })
// 支持方下拉选项 (JOIN biz_org + sys_user): 返回 [{ orgId, orgName, userName }, ...]
// 用于 manager 项目分配弹窗按公司名搜索 + 选公司
diff --git a/ry-vue3/src/assets/login.jpg b/ry-vue3/src/assets/login.jpg
index c3225205c9922f7cc35148470ba2e75cccea39ee..c2b4cfbe1584d3b348eea90d6a27c66dce3cdde8 100644
GIT binary patch
literal 489615
zcmbTcb95%byDl2rwr$(Cor!H5lT2(+Y}?7ib|$v%aHjiC22%BJU9>#5JXuS2{jN9u$zB}3Jlo48pA%%jDIIwcS#+0btg-A
zFB4Y_5K(g{QwtJV2NNp`H477SAD0OW0T2*yOB)RxcO69qJ~Jl=CX@fjFnK#T|3ia-
z2nc&So0!>IxRaP#SlKuVk^#E<$VhC=1)(|inYFvSGaoavmzNil7dw-as}(aVFE8(ZIM~=2|4A^q`8c|pcr!Y>k^c{agoT@#
ztBtd}jguqEe;7?nojlwH$^KdT-!3>fD=PkPVn?_C6{vq9WA-+2W@cq#VRmr%k6-^K
z?dGm#@qe4~f0TC9@Nu?aRQ)V$=t@Ak6WCbN0dd9hm}>7U5b^JRZNtZM@ovDMNEQSN{p97{C_z9
zPgr(w30^iXHVIJdWXa|99&DyZGOo
z$HMVnk8%CiHSR4k??9m4`(!0VHM}>>doJtTWgRK8j)ayqlcf%moNgg|hr)^bmCsMv
zjd8;F-^^m|W5<%qO3$nKWcfmV*d@JrRBL`NzulYPZ1MjZnCe_3{yP1poS6+A6eI_H
zzuu)}j@@^EyDaSd{3Go9y~}I)blCm#{-v+)+xm3LsQa5B)95sGd$+c)>nHHbti-2B
zcGzVIS-I!v^|J;@-Ul!*|DFVJ3V29T@BT~y7`3H8m2V|X#lAqURkk!b$
z34HbM7cUN%1~(gjoPJ)sUnVV;QaYN}E)FTz@Za?!bR~TESl{fve@eD@mcD@72Cq78
zR)4Qmx87_3$!?e4^DK9BWVR3erT}BTMmUa792Y-tHhFGN&MmA10e#zP&gmmP;sNV`
zzQCSuTn|ZrmvDgiMVEPB*naj?IR8uDleRZ=A5f`h6$$GOXz3#eaPngw^b-bT4}Vs7
zGYgjIUj)2y#Q{-xa`
z{TctVxt;6HJZu-RDpVPhaJrM&`&s_mdHY@iqB_Fw=eMDWV132saqG(sca?{Ceqg=x
z*i2o4M?-L*9kOt%?2TD9q}@w=faW+{Xr3?A(@W16P_XwhJAd(V@XF`gsrx`ET6_0=
zV+;V$Gg{1pOY(3cyt}EZ_6srE89*_r_Z<8@UECbFq{@w7ef#|K2}|E_1>vb2P?nDy>l*sch754A|;9n|l$F0V>
z?tXnu1?EJW1uWvk`BrLt)W;pFiZ1hU5it!0zCLMQkeXSzUsczCeCeD908l0Y?!K~t
zFYrLDWnPPyCOhFLv-;(o@A3ZLI!bk~Hs*s$LKXSmx+SFFZ`-5KVYSbJG|U7E9!K|F
zJF}f*u1jKdUIMcZgg;+|KcD*EiGO}|8O_$$0~LD>eRgj?i?m0gwGF(OS9aQ50R7bo
zn}$l5HQyKCZ%@LXn?HftZy!hNs-r;O-k(n$RzHkpfd=`$?|>Jd?*pLElgTD^y|zSe
zHDclD`^`2$9TeMYCN93!yy-?1a^?lGN44nYyJwWo*U7uPiytWGEVjJ4(`6*O037#s
zO6Yt2e1ijq>^@~FsIoP%`gI-HJaL`1`X{<{U$FBbk+^DmcksU&iUfpIYzOj<@dQAP
z&ZA2Yy7AR)_l*p^U$~cGNl4#ji%8V|Drf3zirC2X=mOMTo|#9nZ!|)=knER?%jz{u
z6}PdcYhOn!I{h3n{+t!$zB!1;SGL#9L<`QGX}CE@eTn3)i29Kro#w@g5|{AXGY?M7T&&;30zfZ4lV
z(vG=%>mu?B9L7ha9M0x7k|P^=g!yjkBBKIxZYo69M$211T;>QpAT0i<+*4AidpH}n
z8G~U3J-~6*f#|NWXWw-fXav#;$#$ESOZQCe)R54%;~ub!j$eGc({i_Qvv82ajT
z@#BVou0&mrNhbx%qE(TImHu{-x>*)iVL$*IkK<0y4fMHO03)|J&yC#$r;B>wOK}
z4Pvi!0b9omNDEYwzK4X66|?CFr?&R9J}DqJP*qr19IonGC&mDXVG}^5Mf$9@`*LO+
z?fkeyjp-2QSEYP|FF5TBO_}ocZnCHO`Q%2;Hvb*I7vVP?f@~ov7@y6q1+_(rsY{)L
zu)}>dYFPYf{b7A*wThd2`WVnpS0(Um9kYc0yjY#THyQUbcUgv~btzFO#G>7rS&{Ee
zmm^o?(sfyzC(+W-LwrHxNnt^^l_?nk&xkNoGzw@6RM4pMF4C0SD!Cn9*pYz|s<^4r
zbIYThC`jyPTCoqe($?jakFT?~yxZH8+s50HXKb#EyzBL#?D5Hbu5OW{k)-W^Wkod&
zfFnm}+YFe+rUTOQyw$2ku?szxLZdld17K}GGRB4d^Nnu!{e@p5JEK1NE3b{77gd+|
zx%l87nMfMtKW`=~w0@1e0G)^cW=CiK$UC5UErj54l<8Uy_nUSvd4a>9XKc>+cl33p
z9MwC@CA@XbiV>if5M^Wrc~6L)$8IEp-RyhaV*}T$8okc5MZleU3518L(%4TNZDsJr
zVq#5zOSMG3mn$ZM+t*{;wNZnRUbi2tCfnOs;Xa)8hhbU7F16h=v`2h$uWHmgC>yN0Zr;To9XxmoRf{N&2O3i?wDtK0GR`KniYsi%Uw#5v;L>FqP?
zn%9cqDppxV)|ros!&WF!r9O$#K)i;?&F(pqf^?S5jltn>1%gn%o+V}VF*=WNGKfp8
z`>PwuOYdIRfL!2m&pSJy@BQAZ(IVGnmx{P4>Lx3PSMlz|5V1w5Dps@$;M|_<%;tCB
z`@}jTfV`W&&gzC#Z4cC7n_PstD{|^(R~}pS>K11_DgGOQrQ?|vTN*{O3y6IRm&)=e
zGnq!2pJ$jxY{Ak#u_J4|mWd>y^zYuG53zKdpqu+g526x~=bfUyse*6?55D
zz)p9s@a6Tc=~kBe4rNx+5?RmK^{#s^TN%K3DpzQ^^1Zv|_PVhj20!9ed~~~sQJP-W
z^C{zwp{iqJaOtX0LmQ|rZ))#$A!AEfUjqFz_N@AvvmFCRx`%tz@fY6>-la!n&J`Ab
zemsH3lfN`Fwa^D{F~DM~wAO&d#8%Qtr7-`-=EC24cUL}pWj79ys)!;$T3bchWv@}!Sk>#}y{{JIsVrk~H7SDED*p`>H|GdnAcm*RE%a+Wzsaw5t3q$#SMCX~YR=6iZK|wk
zX*M%>@~Tey>fO7N0JVcH=jDqIsFQl!Y2h6O##YA;+ieVY3Hy|+Mqf3}Y`d_Bd=4#(
zbm|(NLXE;I9Md`~bi|$ZRM|iOXp$DNmMLoB%VBrnF1y^+2B-GuQ!RJ`eXr6VE765E
zEuk}imU-eyB4ES9^Ao?QC3os)%8ijY4&qMY-WoGo3DG
zx9`3aow@y{`dOU2bvpH~c|Cad!B;N$3sGpMny1^r0WiZcgEYFJYxz3`=V66fxF^rd
z=~g1DS}o7RQcx;emNSd1Vx);W@Wr`HJwLwBp8M|DYx6>%(hC>s$4}o}1%WBu;4?2R
z&dK(JldCVZzKlxt)$%am?SaP)jHDFRW~$SAjlGduxB%soEtpuq$P%@$GvVr`h
zk)$-bb{8oxPJW(*xp06|Jlh}5W+i*|-6liK;IWPNI}%2%k=Cpj0K^#J^74p#Y|HHy
z>ZLKvrD+kqJIg8_Y;y%8Arq}A@vh{uk<9b2$qRnv?-Qu-o0z}0ZJAJ)sLtWm!59l1
zps`AucCI`4z_VgM@@7B+McI3hrm9fSVJFC9N!2g+rB8SKM~Dq)!7}|^uaQofD_wj0
zX`HQDf{SI4Y|9ZpBPqnBovHiwm#V=1kR8DNyHFf>Mx<-;ra?*lP$S&bPSt)j&TDUT
zn6YN8kkT<-v+Dze^0^52JZ|^LtrpIvd7~TDv)L7bP^j6v?YaagzVEA(LMz$;CDuE6
zbJKyXSj2(OR^`B3bT&AAlSwXd6=ggdM#Ol`q4CX8n&j~XOXclJ;i{lvDtG{Ys?;Na
zG=KO$1}C<9jF;ne)Ejqls~<>@;nm`5)vy#I5yLuuD^f4~vT;d!1ByZ
zSWdrX6x`onK;-6PIYsy|CW;?>4(!7senh%9D}bQKso`
z!)|ymHw&s`^VGn@i`Ax1NGhT43ZMmmkl7}JyZX<5tyM4%YE;-4?j+4srR&8xL{3~e
z2Tasf@zWZaM(y&&os5FiJ*w?rnw2Q@+Lkh(exT;z`{p93r=DumM|m`!U<75c&$o1I
z{Td4)jh0}VgScS5R!-D}*;aTlSx_}xx`#wmjq3P}OSQ{n=5xL-*QHWK-Btu#_zzLO
ze^(DN9jrDXzqMF6ZYqYf49cB3_g4bzBR@2}PhM%gyHxPE7_`L6K=3rYvo~fDiJ4Tf@
zZSSq)3QSG5gBRPYg<1+*f-Grt*FtQyJZ&rba@UgZgr~7tO(IxZtvkyl`A-
zD#EIa)^45&@W=$C=LuTXcMxCqR}KZ`YkxB;y}oa@O*7j-y!ejcbk8SvwYPX1lFjt~
z$B`KYqNG)6wm%A$1rw1~w1S+AN!L~8_>}W8B<}9{vYKW14iFWM;=
z2JSriRz|d!ZI)#XP6vC|x(|5iUuVvOAVL02DT^<-B6f5x%Fx7Gx-A=tFq5BlQEgsY
z{SsX0S330N^c$O%0y{>giG;&b_Ax+2VE{!Dyb~$VwUN{=@mt?ZuueApqa%fg74D
z0(xRNv-s0eFc;MQ{RVGC;k7!oA7H<$i;50hGPp1|YFEdPqh7X!c|ztfp26gRemaWP
z0c=p-he>(1Gbb*CGyXA~lxx9RX-cn!HDgZ##XO~Oj-3yx!o#OhSF*fH0a+G55`k^%
z>I@b0`kQOF5k@PGGe)^>r)31fD9s?AhLY+gYe3`usZ>y?@W
z6I7%jY-Pu7y51>nDdvt68>S#ukisqX@Djgp+QkxDsn6NEQfG42YZ
zDL4|bfruaV5E%}_#Ssfsb2H8WP=BZ@?OcH^WMMfJ3F7sWM-Eb(GAfV7$bMS}YX;1R
zlLTNpfacwNi+iDPJ{j>=L~L>q7;U>>YtH^}eL^Dsx0#;g6)SVg5KNQ1DHTujxn6PKFzS>eVM
zTYw6(+G8LahIpEGhprkVs>BGr%@ad9O0WWnhU{VrywVAXVk|Q?
z3lnk4*P7M!%j-{5Acy&|>7cBsp|X1Ds60rl@mD@yd6i}mm-rQIx-fMDt(~&5dc-3L
z<*-3dpr9A#u*L;gm}B{^?AnLFTtE;nZobKxxmQ{`@V5-cp>V{qGKjo@APJ`mXi3Oa
zAmR#)u8j#|*VaIslV`VUPnn(6-P3WBkJ9qp3n}_FPb%%cFiT@_uUrwov>X{xGU)Fd
z{M;z_L$FtEZjD2<#nf^tW@Xo#HBLEz)G6x9_LGoq7(cEI7JjP0;^kZ?JExe>yY*`H
zNpb}NUka(zqVtm~+cHEo^`Z|#JJhWEZgdN`0b!CMgH{{Pi86QGUIi
zi*8aUH1OP0OIe?mjs5dx6;Hm*2CPg9j9c8bjK$l3=TCUfOE#xgVYekTXOKDUt_jN@
ztG8BP5tBGpUk|0j?V)Q4x$O&gF|n-M0#P`xCZ?@85I#-tN|hj0UTP#OkP%KXGM1`x
zCBDYU2rvZ+0j*r_taUerIH5&{idB}W6jlLxe9l-(of<9|kT{3z_#Z=DPR3WNNb{Nj
zSOOl^GumvoAx)mbv!k%sm6{(0YK%#+Tiz-d$R>u7nu{z)!i>F_ee){cfd+ya
z%B1X3R8lM}W&NV{z%J`S;cB;Hv6B0BnWsT=BH&k(6SPR})R#vQ2$@mgnb=OVtkkPZ
zsB!{s1kA%!rCt<%b$H@y>TmcP+6o`E4g*j*W$W-v!d}CW(uI#Y{tG_*pc22IBTg(v
z-1TI3ytM=ISvN+u@F7ImS4PV6SlncO7S1@igiT%jt(m#SFeXYA8CulT$fyX?e-KfX
zd`?W1Jy9Wf0y^Qh%Y^`oRuW3>&UmC(Nf712IuM!)e#~7PSQ)5%g`5No3Jv5I!)R**
z#=iNB6~rQWP#QFE0FeN=mle2Q6A3&Q#wLh|2vMmS
z%>}!eD5TAulB0K
z=(g!M$Y6&hAYohhKoOe{Qc!oJY`Fe1V_|dP_KxVqlj;PZD3-h9dr=8#jiVnnJcs;z0eo$W$0ahP`O!2S&rh
zd)Xv_>kk(LkF+MQmwJb<#-9qmg|0{)V;5y@oFDUr5*-RO_(AkP!f^KpTs
z#$d#?=u_iBD2ka-?V2YlvcnKld|1p~qIb}YrvlMgA~z5edv!I>svK;PZB$E;xL_@x
zvmo};khmRv7U|;)u#Xy7eN0FtVlcc@&T+r2egjHV)IxxCcm37Vnmo%${a_S^3ycB<
z$RQLXUU=YyV!L6$7Wx~_l7*5%zXDc3ln#g(hZ$Ke9(4-+s_eWMUdTvzx4ZM*(DFt@
z(JxH@c&H3%*jAD=y?ikH)J^HB>0SlhPlQgt%Dyb{Ua1TMKWY*%+sPCa^1NMl?of|x
zT?g@dWGq2OgaK;APj1h~#9F%tR@HtOA;-2Yu+el_p>hdPW2
z4*WH>nK&J%T;Got5iW{`b?7IJZ?V~{gQ$$~m#^Z`~oX;~Fm@Q8IUF&Cy8vvz$H
z@g0v2;wCU%F%6M(7X(q?Y$jm*c7PE~5xAqC{nn_omtTR48qGbPQKa71@M!wGIM|76
zL)BA5ma2;p&=!V?L_%uMe&l>ivIVhmBKd2}Qi!2_R|eQDvR&H{e^?yQ}GRv=ae
zSIJf0H&Hwwd60V847p^MMRvgOs!(Jj-=kI=&w2c=4!mz%VK0{fCk`G-B#~!;a~n;4
zEcW52V@@=A{b>*9Z4zf`m*B>vC7pvW!lbC65nKgXDj#+VZwyEj5zQibWt}M}b^;Q^
zE-=AxhU%$ofJ{Z44F78Ze4Xi=Wi~rF!Us96^O?QCl3D?w0J0_llB>dgw3XV|Q0w$-
z^|5d5rZWq+B3|aSBcRY^@C1xKnrZd+Q<^r3Pl@I$t4tx8MMZ3Hy*CxPkQ~n{Tm@df
z($O>iE1UL%XLz3sdT}i!va7W!vY!NM4{glFW!`p46vL;z@H(A>=0)kJ)9
zWXoyEjw7r7#9vT#%vv_;6U9hz81$Yw_JTCIWbY5uSw6I>j7Fh#Bt7oJX9Fy5A=SjM
ztY>lBZfZG76l9;LI=2QW!d)cF@CSru-xiu2!Ok3C!4Teb#$&vhCs*-%Y|>l;l(>&{
zDHtki=|7AQs@Xvd61#_bHmrjwj1n53T=NGQsCdm5#BaRlvE!2OA}MGz8!HT%Vkart
zZI+wSUJ&MVat#SRLAwzJmc>d^@(5UtBCj&e$MHIcjB_GFnbl4jHdA&-COpDbD1_F%
z@pQ0{*&x|{#9usbo#Zk7Kx|$y9IHfZr2-16PVUJxrLB;I`?5$yyvnyBb@f_;j2W5i
zC3jNS;zqRdq|WSA_{fLCX4Y3;Grq$_8NMI9;pe-k&~p;-Cnlss6C7u{SG!BX*Y;=D
zO)mwzk2#vl3UJCQ7U)(gzxG&BoCumnKOR}|MJt2W(`{ns?Rlthemsg@q7hY;rjUzs
z0#(}SxrH`wYl@o8G19fk>1av0;LK^_GR}JYZV%^z2SAd~KwTMp<7reT>ro__h>BE#LlvmH(2zhQZrlwod1a>cBn{#?p4Voe{XmwblZmq3
zm_VvyeIC+_JCb9*($fAy`X(x+Nl>6WY8)ya9}Xz-!b(svPT2^-KOjTUzo*du(2uVn
zD*=hF#WhxeSaQve==qHci7$V4N>dFucwK-AXq=)ESUdNlNoO)av7?NP7R5?*l?8%q
zE%o1M3`Pvh#%y~XW}eAtDjkgu=ghJNU*>;IxnWg+F;WWShpBVAa(6npB2bc9cogh=
zwxgm)W1Y4ptkzDz=$A8j4i;>dJedvUOI&GKF66%IbYR#{o%iG?%|diAi!2Dpb3hum
zz|j0S((Rb}Avk8=zu8?7Y8v_z$dXWCuYF!4DJBPMpb7c3xUJ2)G}~gfbb~hcYfuIx
zW3yq9qs0%&BONTXh=`;mtDgnHEs8(xIRo#&`M|^TqU1qq#xu^M@|#P|E;*eS65XKu
z9LHrbWE>X~luFTs8
z$(NE^RCv!`LyE9U%9?A-k;zZF;-Qfsf`D|YwAK&1G{iJ}d=D32d;*baiTa2?@)oAJ
zZCP7ecK-&|V^F?z#UoFRRPqoqrMQrTt|f(Q
z-vaV~J{fkj8ZHBu@W1*AnhVOGbE75;T_;G6DmJOKXCIy@ffvgo=*AEK
zPP!gGWgKl_L!;{#d&^wsK}9@t=`8LM-XVenPuVIiwX~Qiv*SYC(b=iLHlWS?*40(k>G2UpH(%V%=5K~`_VWpBP!^N>vuP#v5
zgubeqU*6$k5)roI`DbYAT~)LfNJnL{%0PZ5WFrjmb(MzkFSieefu@4_2w6WJe}^aE@enb;-k_1Fh)pcDZV|
z2KVB@>`B?B*SrKd&NS>M`SAkYbNfI*s6J>L-aH%|CijWrE~x+E
z-nu^44CZd@uHoI(lXb;#^mmPCxS^igK)l#ufI9IzQSTDMTn>e!u5_sR2(5?W(QUvT
z88sr;Q_>N3{YQn|9hI~T(>cE>TDD}nR2;42Z&5BvIrH&k0VB$Fb`~7hV}IL`JOag$
z9l=+2I`y3#p-_OrIN5puRUsVsau<>$Zzl6tx<rk%&X!;3g=!H(EV9cFj113;m$`?ZD9zl6I)T#^v+`+m!byrGh!0S;amav
zyNKHaR*}2l@}^2FMi)c=BIL?16Xz;#-4$k8O|hR}?zGv(VvlI@cR}qKIpYFwl+2Xr
zt%KVO4stUB`aJ;ffGRQNM>N}Vit#ZLer#=mHaehVd!eES+69DbbQD4-3|X|cU3r2@
zPua2-PP1x6O0v)DxHjXn4nc^Y`vm_ME4}NWTp(6d`!?xHIw3a>{*PLem+Z^P9+*ToCWrZ$6MM-?xg
zm_`@l`x}flea?|oYbNS&njis`s}vEJ3sKGySi`tDDWgfBC(U}ySyH=E?=o)~Q`pV#
zDj#g16rfwM={9Xlxq8SN1t+G&_x$Z!{?JI!2S1tD1>W%mWEADyn68h@UNsHOs`U&!
zeg~QyORZDvpTb0ADUfK5gWRxdl2NOcr}78IY{aNgs@_fffNX;q3}nsNy$s@5&(oN8
z6cs5YxHtO22ec8cDc!GYpe+`v5O<0H%c{DK&QV*;5UeOywKJ_WfeNtJwGfb7mEsx1
ze$z0DSV1q{O-a1C4yTWwRfQp+Y%u%6shwVp%?jmVnmNBr`)S!xy!k5%SIsI=AraC@
za`5WU+O~|-z22r|sT~9!zSAg@peY$6x%2(UtCEQm
z41B+Gww};!hg8)V(`NI_lrYQ<3~rf5aQJ3%sGkx^9d
zHL#^>Cm}XXg$@RuGiGBnxJw~(aMQmkcwUveGWo#~e!-HKM-aO(!Y-jWzg~hTS@1Pw
zi+_8@c=gy*W%+(QgR-BK8MM^8({d=uvpg}nWhq}>FbfV)`GXZ?h+hfOv0*`3O#QTo
z4zrU=^EFbl7IDN~GL7AWyGz!#@=+13j~B`%oqZ$Xl|oU`xp}Uu7Z_SKm*Bwl6$^SA
zsEqKB)vEqCxq&9$bIB-`;@ZCHNWqvrzb;oO9wsB(DAc)2R!IRVPS)p~#40vOGy}>A
zs{h`27^$FqqDYHpj@uK+C%N!ae9*&RgaU0Q{U{oBNNh)_uIR@g
zy$nT7mlIow=W>=Ihw+*C1x3&jnA_Pf7wQD9emZE%^|O~nyo`WiO~!Z<269wBQ`IAFOx54Xts>dYZ$qH|&uD^W2V4yk
zWwJ|WWo>j5x=5xe#?%8?-IxV{3DCss%gFi^VkPZ+Sn}tcWhQIE>6=)7M2QVeG}`{y
z-ohjoibKe+mt*|0N=ZAHY@|o?xXa>mp)5rH<;+G7$e{y~md&MU&=0?$Kq^c&
zZC5b0uH+Hy_v0=Z#cVb(@e|Yaleyngo83wd+?6o~HKXF2hz1q=^`!%&TH?b^|Fxu*KqTG6-HIbUTfphgs=w%f7
zD;LZuX;0JQaP9>?0VgQ(550w=?aruTUwRwsv_;8FKIC%E8qrx8xKMY4V{b2rdCI{?
zQmvDYhZwScq22>p)x@Pr<-&H4n{zi_e!5uF;2rm5{vuXq&)Y#w)b$m`28?XnI(pb1
zT+=kMKpJrjl*c>iYTv*1Mz~hoZPYhco`jAy_zG%vdaFXBE%{t6%6XlU=^4_IPE%K8
z4z%2-MHv|Q1p!<~cI!*9^`EkS!*gqGQY6J@DJ7psm>j~mwP-EZG?#pmH<3yI1=T(OEr3LAo?v_6Px%j}>*T+>yK%}%?&B!F#D&`IW
zZyTw{9L?v}mTbeM&w2%yewLqZEG~9QaIr$*6;7fSjD()%>O2{$w9q?K?|Yqo(+qNE
zYG}`=UU?kNpE$id0yQ=dJCZ7lL{?g1uT>Zt@fuJL;@eFLO?v52qX%jMBD!1rws9k)8z
zGd79M%L?C&py5&I*a#R1rEP>7=O&+3fSki6PA+i~-#P>LVgb%MuSu=|Qo@#1`w*$^jUruKF^@HP(8?!4!*
zA%ekh{*q~NwPv8i?I@WcGHb;P2&hZJ{(_i|gi-=?cxcL0W`z79lMpI#OPn1Q2eNQ4
zMrJ(_dA(?y$EIZ7z)wI)obMPEGT8?bi*L+P;238LHqgmF7qpi3_gpsP!c}7i=73bn
zeeJX^7x6w>jzuA!4_@C8n<`%CgyLXnq;!ScU#fB*O5EvtE~M<&Gc>3Dgl@vHOS~y2
z0bABh|s#0d1_OW>g_U)T*d8!3XU<{at@9S+1U9z8z2bXNjdW
zhTfY*5}a;NmHknY|0||6SRu8w$g!Tblerh+p#UG5!#bLEm-fom
zGw7#wOPTSM1qm^cA$I6$D);U7m;tpkB{VV(wX~aSJ@*B|n)a~<5CJ2T&P^awrW!PJ
zE>ok&?k)n2()*Z0hHq3tCiP^z3YGyZh;jXyzg1G$oG>AlSdD7I#`q;`MhMv`6iYM-
zC6*it((xmDQcdbCleLN7I3UW3voS(6ODiJS^~oy7Ndu;Y6Nr$zj>WQc4(Wua7drK2
zi9qA)rGbSu2ZOU+|CL5_p%DvWr0uuYzCp4}Y3QZ=X=US5MXp!_4B6@pTU>x%qTK;&
z#nvw_>f9PuAhV)0Ev@I-!{19ZQK_pMV2-jx%1Xg7%sOfO7)mLS)%QR=lbTUdr6<$X
zv-esptv)zVtJnKk;HK6`KW*)7Xs^-DmWj%^2Zp#=fDsT5f6={VA@w^3R)uU3QXf_U
zG?Yzyv;Cy;&IbG}`-f$lzlehdsH_!YZoo2yGYl-zrXFR{;)Mn=9~@-?NDB@ay;PAE
z$+N=5_T04TshV;_Z>8q^$e?VN>k1q%bHr(Lw-i~r1EKM*LSm;4dx&sWa($J*;({bM
zyi$;@|2v2arRcDRCs_yCLA~1RcSP_c`r}NAc^fk-X{7zOxa@h-ago{8dkT>ciZP|V
znjV`0xP^2q9ZeBj`oX
z(xwFbe0uV|c;aO|%Hw!~GOpv=vR1tJC0c@WsIc}Wt|p&Nrn2Gq!M`U{V`%Z~Q%4YD
zIFEzGLAD;O)s0JwPF+=x%7_NV;!KsFjDSlluG8rNwL6#cr5#1P;usjqIidt{VY#O=1Q__!s`njO
zho*Y$Z!+w^+3($Ux8~KaaZv1iv-Fw&atupBQ{I^oh6ei_7Wh2=Ec6x38n24}#
zK*_|lxYDp68C+t_aD)NEJi9cY$^JNP`K-}(!LMf)X(4xSI~#H2!sN^HY%mVvU*QcM+&3>MTgiK
z1y2+1kZmIg{B|s{9iL~0`eU&2)Vu?Nusey@KkBAd)lP+Q@I16FP7>Qv_nKy(|a+S?JTXKMc0(#!hKRsfyw;1^C9I@6sf60Cip+3LIb##8VR1f)C-M?_NHF2(S0L)1zgz#M
zL*{BtEi6K+a}_a~II;R*r+x%Df#8cpMFQ+*WVO2(ElI&1H$;~&GI
zE%LA^{6nOC*#>s{=GGX9JIbe;EBFSUa#&6yd*bw0R7nt9;~ywJND-&;?5w89c5Lju
z>!C3xkDz#W+!>4egDPBTDM?9@t?sdy)2oT@oH-^guK)f-=Lgzns#f!r)k4!L^Dvkm
zXhEecg;VvS2PNR}9IszUs&Nsk4&Mn%N)QUldl(}gW)A2T)zT8@|A4EiR^(CVOd%nF
z_o6BW{Y8oic3Ii8fxwyGxxn-Ar$mW0XK~Qbmngy@)j7J-+!3p0t3xG@qLggR#n&HO
zo8Rq_2n|mW-jDfmi`Tu~>tIs;D45KGkUPnX*UAvn(7kQ2YJOtj!uQvPt;TtDi6489
zclY!7f|V5+h6~Ug;U3qpy_wC#jEOHz$&ELh!>HuS3S882vbI
z@u-6cM!j0a`7K!A{M{`N_@1lQ{26VSi?ek)-5JyQF{IqEITcs@W;w}iYBrG&a9c!T
zv~Er{UM&PI)q~hI3}oqV$;2C-Ho;G-YJXCEi89@j;o8;B5dsQS2`ZYAA|55gB{R%P
zFeNZ~OEs5~8C=K6*P}ox4QP)Tb-=6FXf+-5W9~g2mA{uS_?fk^+Z(3b-=(e>=>5_j
zO`hH)09cAo-qpS`3M3ao%q*qOsV;kAU5Mjc9LZW~67ya6;Fo!=l*7%WLIcr^Pf*RZzOW_BC$_0^f8TMGyP*Gr8yRnH)r5gk>HLAbvxE@s(jHOvaxJ|ImLE
zwE)tPYUxCW%*O^H7b5;L%lVsB_^#3gl=7KsjyT?KBk+gP|Bb`v
zM#O)ALzMd%*>fLFiKpNglcV=*&a;D&q0bb`u+DUY>aj@C*_V}l?hlb__;rz|n-p;*
zz6NaAamxa7>X9C+K;@cmto6H&21Y6xCw-h=!BTXI&`k=|L>
zMVMHovH*1iA2}>IONDlT_-#{H%h0NR8w<*q9HdNaN!=IjjaM~BP#MKex-$ReImVwf
z4f*D!dH-Cj`@$gIQTa<_+jhAkBlv?LN9n$aW|IBSYf~4zcnVUQai`iL
z1ysk753i6`f`VEb`xUonjfB3D7$D0f?!MPMaUZ;-cdw75lvT|vFeLWzWV6@yE+ZZ`
zSjPE${msL!VU;*f`1Ag$Tzww@8*>%;ceS*@R(Jpu!8ObaOVvdX>1=P%qpS)9B5kMQ
z3(X{s8Yj+?5&Yri;#Za9A&kUtvr*jW;q)IxegT{VH`NyUWer4wpDO7T_8gCddUK%F
z47?b_8=lJYndOOq(Kbobyp*?r{EPRqq=w1;ehn57#Fma_Igzy;j>vmd-
za}q;jcY!nBe)WAr&GB(RkkP0Pcl^Ikm3C1jYJx}e&Ll2<^4A1FEIHJsm>`H88piOg
z(?3@f(lxO`@)^e#C&*}AbTJI65%XaG_q>-AXXw6%lgGUdtkm!tx
zp`Iwq*rfvaWPTLKsX{-OQtIuA&gr7}+{2;=gW1-!&EnuF#wijO*C}O@NpH0bsX4yj
zfEe|bDUFV@nTVwFy@XjMpc18Z-HdxOC&=ugRR{MkwPi8FEF64m(8&T(%Fj!Xr*}|>
zvlLL>W*W=$dE^!kcDvLaBzV++B_#RDjc{&%*|V-cQ{;CiIm*1m?aupla4{dL3z
z$DhjxNzn{ue!$KTV~?PU=LTWbCUu=jYq%eeYA_}ZDgkJW$|sb&uUUlGV<@ottQc8bbQ^76@$50g;X=8BK^SwP8>Cd
z?L2WDq`*Xi?Hp-tLLS!KBT?H6`K^voTjXgnE4C3RH_9G*N-Me_krFrhiY-o0!eiJp
zXv8$g{u&fScaTbfqJdcl|4p}bc=wf~sIlFLT!Bi802Ml|akxWEAzuBUh_`A*d*y9W
z$zX>1N{5(m2RXVDP6ZJzLp7qS_;|JSqchZmau^?fQb@!R>9CX)!mbX226=-w4IK!F
z`T}8wkGdUUEV=D(g#AEV1d)ft6C3H*;VjCLsO2RaF4-XD&^aWBQ&Rp%Ge96~Q}8)0
zH;P}6h10WiVGeoRvzc#W*gmd#-o;oK%(NmK_iSmzi+=xh)V?^(e*4LwB51Vp~6Z8OHnc+K07qBW9ylcZc#Y57f)CMZjS
zQRXpe1_yMj5=O5eO@E~+^;9^>T1}W2Lzz_;;UwE?5+oWI{IRoLevvR77c`A4-(~-&^-U65HsXMoD|K$@!=Vp^);K
z9$vyl_k|TEb3N#xY5y?!sbI4oA^e6Q_Mc#RJ*Q8jx=5bCAr1f#-wC@$K7qEO+F{}4
zmFV$v+oWa*1_g1_==oE-@oZ$@*uuw@tZ%c_hB2xjAA00)J{UVw?!dMrC&x%BaOEL&
zCJBQA8g#h%0L#*AxyYWooIm^-zY7*rp1x@0P%n_yR+GsRqb-N@?)x!vs=j+cQ!u(r
zk4YHoQBc4?(WMo$ee9L;GRJ$S*6*Q@CE&=xEI*wlhH
zM_FHr{#9#%!Eboyn_9C!TjWsl)VGBL(OJiqagTiMm2D>XT;;(pp9O&{7l*ofEOY3J
z-HbRs$cJ~-BCz=zD1bBVl7duPK3`MMDlhw(4qTO=Zi$HJ6=ZEel)e1F05L$$zX2jj
z@=!uyenPly6nl*>D9KSVJe+}Fi3VwedY7BGA0ae5$u!0Hvo^|b0?kEofmFnx3v~>$
zvgz;PkIN$9eAqjYD!jTJJUIYPNt-ENM}xl4qm9rf0LbbVl;@HEM$0GWBpJt~%0J3J
zhC6`qf7*aK&(xgbm$Hfl$>!bGzvSk$#ukF~z%mM2P
zJ)HHC?B@bjdTiJx6i7MCZ!;DA7HI!$!_EUIY{3SSSWeILA!gq?$d#%-LqYB3(T0d>
z=l7k@7n{K+1;xwdy`-eZ@HKM@6?u#B`WUPyOReO6h4iK_1F6*urgfg02}!P|}K0BTSvdx5DtksTu7`^_Jpi=j)+iMmJRd_z{uA`T6}H?H3!kyUAVL!Ox1RhwKZI
zYNj@LV}hRAlnnVN#=EXy^#jh37tHSS37~2yhYr2cy5R=99gahLDC~7Md~uW|uoJkL
zm0!de7adUFC2Kyt&O^>zQ^&fcIBae8e^b>-f7S~RU?wd&d>pnX=f)a_r)X!?Ns=-t
zIZ_e}LKNkMidq6ZMz!36BO{8sL!)5sJzHS}VDj{fj_*2QeA%bYu|=$jl_aC%@SG}K
zAZOm85lw`olgR}LlBpWGrIWd`rZ8tP}LZdi@hKiF8wn%)h
z!1x=9SxPcmh>*0c#WsJHHbsJ_V=3RtVIZFw$E=d&^KNzGA_aFLhf)Hl#N;>ZF6G5$$R%
z=CVe&+4r9wVuNxNXwVLVpb#2SnzAoJDoKp$q>s^zHhK3@@aCFe#UTlTtyp!?A16g2
zCvna#WZm5)I-JA9SOmqCu{DDf#U&h+p9(^~;6kI})==z!x{U^L*Ql)R;(#w&0ZX%a8J)
z9kSRY&pq916|0fkG4jSC4AKYje5!c(bbu|yX{U2VBOaeTNgrzJi-gsYzFZ{3m$`h_
z{Q3IF{OO-wKmWBmdx=-5?t%T|*(TGcR9713QKJw@64c3b|LUJTzy0&C?|=FZ2N|0J
zrqrK4l1-!D{?+HJaWYv;O=v0emDRLudk_o(I7x)Rvs`V@HXUwy&_l%<8#h(NcCw7?
z;~)rJ`4egF@(TsJcP*#t(il9!tBRsMsW?hP0X6&=VmQl$D6niDM9=X`2RyM*#_+yH
z@eb8OBrOEMT3x~m&XLVXg!nyZW?d5cXoH)YBGQB!h-GdyoM3$)Q3}x5TaRgv0l{^r
zO7Y6Y10^d5ovD#^*H#S71)=S8LKF<|`w9k8$_i8U-vat{HlQicEiGfFa)P}fV
zr_Pe?W|XNF0mTN0Db|V5wM#ro!JUK;Z_Q|*>G`U$jJ`sIgP)b<+_r6?%Q_h>R(a`#
z?@JGBhrN0n6k|`zC
zk|3vrMRg31Xlln{PCRYwSJ_L-aj^Q}Z%IOdj?KAMT=d+AlW?33r^p;xqf(hinTeX0
z+C)i4Thmn-CRWm0Z1@1zypGR0l(Iq14G6v<;3sf5deDhZ%L%D`os@76Q1MBV4b&3?
zt@xZuCb6H(!@%fm@C|1
zw3q^=jhuFL?o+spZdpX~yi3^5pmS%!e6q1t@z8K+mr$9gtyraAv6Rpm87PzJ@LdBY
zN|%htR4xu7JqD_Xe+XsH6sfaEi>!Y7pi-)8JqHw3trlo!t?{BHm(g|=B#_m;G%(px
zuy}|zO6rzkGeKSNR!6}oHrZgOT5232=}Ihoy09sRSf$HI^>R0s0s?k`?>OBgL&@B>
zBscq*2EiIvunTq3g7O3C+})7rPfGKKMfS~PS4SiQeexsO5T-ne_|19<96^5eQOCla
z%1fg?}Ajox+Nw%>_M;3
zI4D5%bn2b7)q}G3#!s;3jW$73~w)FJg+7paD1a;2u1`s
z+Q6Dmd(UmSj|?G#oD57RP(eVj|ia%LZ>Emv_+dpsfrrQlY|e;GPf(CYNK4
zH8OrZU;pOYe>#8o{rtu`VT@TN)n)1yMkxV%5TN*F&+D|w=KUK#zx{_lzy8mE_x+pw
z#jS0iV?bC>m#?L()mP`N#v6Z5apKFC!fEzMt6eT|ilBr4D)RZ#0>~wfYFl-`&wqXLu50cm`CkTBM(g5e{PhqMNYl?NB?3P)6ukXGOe;MGNht%4$wl!~bj
zJ3?$k23mt*(7CccLSh>;A|3W@hb)S`QOVIcbZ^DKrAs`!T3S!waC2rUD>>;ws23%@
z0ObEULaxZ6T8u%o)^eDO(WK%?MOX|QUGkKG+4b2Rd&@@oc1ZtvGxvoNJ_;$_n%o?~F}VN&
z+$%E?J14V!5~wo^W$J{Pdh4SpLW{`
zp7W4q^KC8H{ZZGKIbwv7_?Py5okV66?pMlRt3CrV_9=sVJ3>ha
zBnu{W?h}H2Cl>1%JTIRa>gPveqPN_iADVV&
zQzE_k1>|4(07d{Vh!kG(z^S(6I6W7VxF-lsXZ}~Y8f9TM|Y17#K#R;g&eXFx$MXih22$&*}asJ5P0Ei
z_MYX-oDD{NygM?%$*GS*$2mL8JRhTaKfBa9Qqm%tE-SFG_aDR;_{D-Yl;XWSln3*(iisv9++o!w9
z5(a-j35hEII(vRUx^JU&9@k4Qci18K9uTKm|LQf3!p))~@i@idK?>VrMVPK9xM^X8
z+a_G>ZWg2>LR_hduq#!Kk*oIAW2@-Xw?C3r}^DT8(N#pdYl}
zAHw%2d~2=*??#9v)^$f5x=w!J3bwXJsOyC(y${
zpd#}M+!`Tjh~}j5isOY;KWD4Hvzu&*GIw<_S9+IfnB`F7Q@$SZwlm1kvxq!j|KFc}
z{>Rtz)!Fuel20qje61sI}02_cYW9;Le;uW|65m31N(Yr$+tm&4fKm!9iWWcIys*lIy
zG%5OpPSDZ&b>A1^^~6wtK813Wf7ksB2(J$%Qad*!^=MEhgy#a`Np6R>W^G51c<407
z!_@HAOp5%j5B(k=XMR+6M9QFT4qXCs7*Cb2pRF&4TnW_7X;%)6m+UGElRh7sCwsDx
zxkbuzTq&e|mzNKNu&`E(tz
zT9H4u?hd+NVv|!hlhG^g1XbWiZ13*%T6d?cjYoA6lY6o7)$@6No6_6#zJ_PCCAM-M`
z_H0`%V8%_+vr@{CTb()&w9_J_lX64$p&JQzXo5!w=`_*9R?bOf@l^O0>*oEGpTEyv{>}H#U*~5|mr9%#?Uod%2~AQQZgm{qSH7xFfEVxA
z`KSNn=U;z5zm;`JqTgOf;4w-H8g{6eRZGb+h9`ZEjOWVPvsWh7%aSo?xPj(IK=O*J!5TdC#BB
zO!D`%ycYr-5k^Se`_Q8!zT;a^phb3A?J}l