From a6fa7ee721b028f5008972f38d96dfa4c80708cf Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期一, 24 八月 2026 16:33:35 +0800
Subject: [PATCH] Merge branch 'dev-0806-wql'
---
tcm-training-admin-backend/pom.xml | 8
traditionalApp/pages/home/home.vue | 2
traditionalApp/pages/publish/service.js | 6
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthPopularizationDetailVO.java | 1
traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue | 8
tcm-training-admin/src/pages/live/health-promotion/service.ts | 55 +
traditionalApp/pages/healthPopularization/service.js | 6
traditionalApp/pages/weizanLive/weizanLive.vue | 61 +
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/controller/AppHealthPopularizationController.java | 8
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java | 34
tcm-training-admin-backend/sql/20260806_admin.sql | 59 +
tcm-training-admin/src/pages/live/health-promotion/style.less | 223 ++++++
traditionalApp/pages/articleDetail/articleDetail.vue | 39 +
tcm-training-admin/src/pages/live/health-popularization/manage.tsx | 4
traditionalApp/utils/weizan-pay-bridge.js | 185 ++++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthPopularizationSaveRequest.java | 3
traditionalApp/pages/publish/publish.vue | 69 +
traditionalApp/pages/healthPopularization/healthPopularization.vue | 69 +
tcm-training-admin-backend/sql/20260806_prd.sql | 569 ++++++++++++++++
tcm-training-admin/src/pages/live/health-popularization/index.tsx | 8
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthPopularization.java | 3
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/vo/AppHealthPopularizationDetailVO.java | 4
tcm-training-admin/src/access.js | 6
tcm-training-admin/config/routes.ts | 147 ++-
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/service/AppHealthPopularizationService.java | 16
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthPopularizationAppService.java | 3
tcm-training-admin/src/pages/live/health-promotion/manage.tsx | 240 ++++++
traditionalApp/pages.json | 6
tcm-training-admin/src/pages/live/project-introduction/manage.tsx | 2
tcm-training-admin/src/pages/live/health-promotion/index.tsx | 240 ++++++
tcm-training-admin/src/pages/live/project-introduction/index.tsx | 4
traditionalApp/manifest.json | 4
32 files changed, 1,918 insertions(+), 174 deletions(-)
diff --git a/tcm-training-admin-backend/pom.xml b/tcm-training-admin-backend/pom.xml
index 8616947..7d4750f 100644
--- a/tcm-training-admin-backend/pom.xml
+++ b/tcm-training-admin-backend/pom.xml
@@ -153,6 +153,14 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
+ <version>2.7.18</version> <!-- 改成你用的 Spring Boot 版本 -->
+ <executions>
+ <execution>
+ <goals>
+ <goal>repackage</goal>
+ </goals>
+ </execution>
+ </executions>
</plugin>
</plugins>
</build>
diff --git a/tcm-training-admin-backend/sql/20260806_admin.sql b/tcm-training-admin-backend/sql/20260806_admin.sql
index 20c2baa..bf5e6a6 100644
--- a/tcm-training-admin-backend/sql/20260806_admin.sql
+++ b/tcm-training-admin-backend/sql/20260806_admin.sql
@@ -151,4 +151,61 @@
ALTER TABLE `tcm_health_popularization`
ADD COLUMN `video_description` VARCHAR(255) DEFAULT NULL COMMENT '视频介绍'
-AFTER `video_file_name`;
\ No newline at end of file
+AFTER `video_file_name`;
+
+
+-- ----------------------------
+-- 新增二级菜单:宣传知识(与课程管理平级,父级为课程与学习 3000)
+-- 并将「项目介绍」「项目介绍密码」菜单从课程管理(3050)迁移到宣传知识下
+-- 添加时间:2026-08-22
+-- ----------------------------
+
+-- 宣传知识菜单(id: 6100)
+INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
+VALUES (6100, 3000, '宣传知识', 'live_promotion_root', 'M', '/live/promotion', NULL, NULL, 20, 1, 1, NULL, '宣传知识目录', 0)
+ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), path = VALUES(path), component = VALUES(component), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
+
+-- 将「项目介绍」(6010) 迁移到「宣传知识」(6100) 下,并将路由前缀改为 /live/promotion
+UPDATE sys_menu SET parent_id = 6100, path = '/live/promotion/project-introduction' WHERE id = 6010;
+
+-- 将「项目介绍密码」(6014) 迁移到「宣传知识」(6100) 下,并将路由前缀改为 /live/promotion
+UPDATE sys_menu SET parent_id = 6100, path = '/live/promotion/project-introduction-password' WHERE id = 6014;
+
+-- ----------------------------
+-- 「健康科普」改名为「健康知识」,并从课程管理(3050)迁移到宣传知识(6100)下
+-- 主菜单 id: 3120,子按钮 3121/3122/3123 随父级一起迁移(parent_id 不变,无需单独处理)
+-- 添加时间:2026-08-22
+-- ----------------------------
+UPDATE sys_menu
+SET parent_id = 6100,
+ name = '健康知识',
+ path = '/live/promotion/health-popularization',
+ remark = '健康知识菜单'
+WHERE id = 3120;
+
+-- 子按钮随主菜单改名(parent_id 仍为 3120,不变)
+UPDATE sys_menu SET name = '新增健康知识', remark = '健康知识新增按钮' WHERE id = 3121;
+UPDATE sys_menu SET name = '编辑健康知识', remark = '健康知识编辑按钮' WHERE id = 3122;
+UPDATE sys_menu SET name = '删除健康知识', remark = '健康知识删除按钮' WHERE id = 3123;
+
+-- ----------------------------
+-- 新增「养生科普」菜单及按钮权限(宣传知识 6100 下,与健康知识平级)
+-- 养生科普与健康知识共用 tcm_health_popularization 表,contentType=2(仅视频)
+-- 主菜单 id: 6110,子按钮 6111/6112/6113
+-- 添加时间:2026-08-22
+-- ----------------------------
+INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
+VALUES (6110, 6100, '养生科普', 'live_health_promotion_manage', 'C', '/live/promotion/health-promotion', 'Live/HealthPromotionList', NULL, 51, 1, 1, 'live:health-promotion:view', '养生科普菜单', 0)
+ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), path = VALUES(path), component = VALUES(component), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
+
+INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
+VALUES (6111, 6110, '新增养生科普', 'live_health_promotion_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:health-promotion:add', '养生科普新增按钮', 0)
+ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
+
+INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
+VALUES (6112, 6110, '编辑养生科普', 'live_health_promotion_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:health-promotion:edit', '养生科普编辑按钮', 0)
+ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
+
+INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted)
+VALUES (6113, 6110, '删除养生科普', 'live_health_promotion_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:health-promotion:delete', '养生科普删除按钮', 0)
+ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0;
\ No newline at end of file
diff --git a/tcm-training-admin-backend/sql/20260806_prd.sql b/tcm-training-admin-backend/sql/20260806_prd.sql
new file mode 100644
index 0000000..1b232a2
--- /dev/null
+++ b/tcm-training-admin-backend/sql/20260806_prd.sql
@@ -0,0 +1,569 @@
+-- 2026-08-06 变更前数据备份
+DROP TABLE IF EXISTS `tcm_live_course_2026_0806_back`;
+CREATE TABLE `tcm_live_course_2026_0806_back` LIKE `tcm_live_course`;
+INSERT INTO `tcm_live_course_2026_0806_back` SELECT * FROM `tcm_live_course`;
+
+DROP TABLE IF EXISTS `tcm_clinic_2026_0806_back`;
+CREATE TABLE `tcm_clinic_2026_0806_back` LIKE `tcm_clinic`;
+INSERT INTO `tcm_clinic_2026_0806_back` SELECT * FROM `tcm_clinic`;
+
+DROP TABLE IF EXISTS `tcm_health_popularization_2026_0806_back`;
+CREATE TABLE `tcm_health_popularization_2026_0806_back` LIKE `tcm_health_popularization`;
+INSERT INTO `tcm_health_popularization_2026_0806_back` SELECT * FROM `tcm_health_popularization`;
+
+DROP TABLE IF EXISTS `sys_menu_2026_0806_back`;
+CREATE TABLE `sys_menu_2026_0806_back` LIKE `sys_menu`;
+INSERT INTO `sys_menu_2026_0806_back` SELECT * FROM `sys_menu`;
+
+DROP TABLE IF EXISTS `sys_role_menu_2026_0806_back`;
+CREATE TABLE `sys_role_menu_2026_0806_back` LIKE `sys_role_menu`;
+INSERT INTO `sys_role_menu_2026_0806_back` SELECT * FROM `sys_role_menu`;
+
+ALTER TABLE tcm_live_course
+ADD COLUMN `password` VARCHAR(255) DEFAULT NULL COMMENT '直播间密码' AFTER `status`;
+
+CREATE TABLE tcm_provincial_agent (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '代理ID,主键,自增',
+ agent_name VARCHAR(50) NOT NULL COMMENT '省区负责人姓名',
+ province VARCHAR(20) NOT NULL COMMENT '负责省份名称(如:四川省)',
+ bank_name VARCHAR(100) COMMENT '开户银行名称(如:中国建设银行)',
+ account_number VARCHAR(30) COMMENT '银行账号',
+ account_holder VARCHAR(50) COMMENT '账户名称/户名(即开户人姓名或公司名称)',
+ branch_name VARCHAR(100) COMMENT '开户支行名称(如:成都高新支行)',
+ contact_info VARCHAR(200) COMMENT '联系信息(可填电话、邮箱、手机号等)',
+ status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用,2注销',
+ address VARCHAR(255) COMMENT '详细地址(如:成都市高新区天府大道XX号)',
+ `deleted` tinyint(1) NOT NULL DEFAULT '0',
+ `created_by` varchar(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+ `updated_by` varchar(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) COMMENT='省代理人员信息表,存储代理的基本资料及银行账户信息';
+
+CREATE TABLE tcm_agent_region (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID,主键,自增',
+ agent_id BIGINT NOT NULL COMMENT '关联省代理人员ID,外键指向 provincial_agent.id',
+ region_code CHAR(6) NOT NULL COMMENT '6位行政区划编码(如:510101 表示成都市辖区)',
+ region_name VARCHAR(100) COMMENT '区域名称(冗余字段,便于直接展示,如:成都市)',
+ region_level TINYINT COMMENT '区域行政级别:1-省级,2-市级,3-区县级,4-乡镇级(可根据编码前缀判断)',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
+ UNIQUE KEY uk_agent_region (agent_id, region_code) COMMENT '联合唯一索引,防止同一代理重复添加同一区域'
+) COMMENT='代理负责区域表,存储每个省代理所管辖的具体区域(一对多关系,一个代理可负责多个区域)';
+
+-- 为 tcm_clinic 表新增代理关联字段
+ALTER TABLE tcm_clinic
+ADD COLUMN agent_id BIGINT COMMENT '负责该诊所的省代理ID,tcm_provincial_agent.id'
+AFTER name;
+
+-- 为 tcm_clinic 表新增银行信息字段(全部加在 status 字段后面)
+ALTER TABLE tcm_clinic
+ADD COLUMN bank_name VARCHAR(100) COMMENT '开户银行名称(如:中国建设银行)' AFTER status,
+ADD COLUMN account_number VARCHAR(30) COMMENT '银行账号' AFTER bank_name,
+ADD COLUMN account_holder VARCHAR(50) COMMENT '账户名称/户名(即开户人姓名或公司名称)' AFTER account_number,
+ADD COLUMN branch_name VARCHAR(100) COMMENT '开户支行名称(如:成都高新支行)' AFTER account_holder;
+
+-- 微赞直播间单例设置:仅保留 Banner 图片地址和直播间地址两个业务字段
+CREATE TABLE IF NOT EXISTS tcm_live_room_setting (
+ banner_image_url VARCHAR(500) NOT NULL COMMENT 'Banner图片地址',
+ live_room_url VARCHAR(500) NULL COMMENT '直播间地址'
+) COMMENT='微赞直播间设置';
+
+-- ----------------------------
+-- 项目介绍表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_project_introduction` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `title` VARCHAR(128) NOT NULL COMMENT '标题',
+ `cover_img` VARCHAR(512) DEFAULT NULL COMMENT '封面图',
+ `category` VARCHAR(32) NOT NULL COMMENT '类别:platform(平台介绍)、mode(营销模式)、action(营销动作)',
+ `content_type` TINYINT NOT NULL COMMENT '类型:1图文 2视频',
+ `content` MEDIUMTEXT COMMENT '图文内容(富文本HTML)',
+ `video_url` VARCHAR(512) DEFAULT NULL COMMENT '视频地址',
+ `video_file_name` VARCHAR(255) DEFAULT NULL COMMENT '视频文件名',
+ `video_description` VARCHAR(255) DEFAULT NULL COMMENT '视频介绍',
+ `click_count` INT NOT NULL DEFAULT 0 COMMENT '点击量',
+ `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
+ `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+ `created_by` VARCHAR(64) NOT NULL DEFAULT '系统' COMMENT '创建人',
+ `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统' COMMENT '更新人',
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `idx_category` (`category`),
+ KEY `idx_content_type` (`content_type`),
+ KEY `idx_status` (`status`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目介绍表';
+
+-- ----------------------------
+-- 项目介绍密码配置表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_project_introduction_password` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `password_enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用密码访问:1启用 0不启用',
+ `password` VARCHAR(255) DEFAULT NULL COMMENT '访问密码(加密)',
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目介绍密码配置表';
+
+INSERT INTO `tcm_project_introduction_password` (`id`, `password_enabled`, `password`, `created_at`, `updated_at`) VALUES (1, 0, NULL, '2026-08-20 13:12:52', '2026-08-21 15:21:01');
+
+
+ALTER TABLE `tcm_health_popularization`
+ADD COLUMN `video_description` VARCHAR(255) DEFAULT NULL COMMENT '视频介绍'
+AFTER `video_file_name`;
+
+
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : local
+ Source Server Type : MySQL
+ Source Server Version : 80030 (8.0.30)
+ Source Host : localhost:3306
+ Source Schema : tcm_training_admin
+
+ Target Server Type : MySQL
+ Target Server Version : 80030 (8.0.30)
+ File Encoding : 65001
+
+ Date: 24/08/2026 15:04:16
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for sys_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_menu`;
+CREATE TABLE `sys_menu` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父级ID,0表示根节点',
+ `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限/菜单名称',
+ `menu_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '菜单编码',
+ `type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '类型:M模块 C菜单 B按钮',
+ `path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由路径',
+ `component` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '前端组件',
+ `icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标',
+ `sort` int NOT NULL DEFAULT 0 COMMENT '排序值',
+ `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
+ `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否显示:1显示 0隐藏',
+ `permission_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
+ `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
+ `deleted` tinyint(1) NOT NULL DEFAULT 0,
+ `created_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+ `updated_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_sys_menu_code`(`menu_code` ASC) USING BTREE,
+ INDEX `idx_sys_menu_parent_id`(`parent_id` ASC) USING BTREE,
+ INDEX `idx_sys_menu_type`(`type` ASC) USING BTREE,
+ INDEX `idx_sys_menu_status`(`status` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6114 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '后台菜单与权限表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_menu
+-- ----------------------------
+INSERT INTO `sys_menu` VALUES (1000, 0, '系统与安全', 'system_security', 'M', '/system', NULL, 'setting', 100, 1, 1, NULL, '系统与安全模块', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1100, 1000, '帐号管理', 'system_account', 'C', '/system/account', 'Account/UserList', NULL, 10, 1, 1, 'system:account:view', '帐号管理菜单', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1101, 1100, '新增帐号', 'system_account_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:account:add', '帐号新增按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1102, 1100, '编辑帐号', 'system_account_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:account:edit', '帐号编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1103, 1100, '启用禁用', 'system_account_status', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:account:status', '帐号状态按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1104, 1100, '重置密码', 'system_account_reset_password', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:account:reset-password', '重置密码按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1105, 1100, '删除帐号', 'system_account_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:account:delete', '删除帐号按钮', 0, '系统初始化', '系统初始化', '2026-03-19 14:48:43', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1200, 1000, '角色管理', 'system_role', 'C', '/system/role', 'Account/RoleList', NULL, 20, 1, 1, 'system:role:view', '角色管理菜单', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1201, 1200, '角色详情', 'system_role_detail', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:role:detail', '角色详情按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1202, 1200, '新增角色', 'system_role_add', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:role:add', '角色新增按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1203, 1200, '编辑角色', 'system_role_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:role:edit', '角色编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1204, 1200, '删除角色', 'system_role_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:role:delete', '角色删除按钮', 0, '系统初始化', '系统初始化', '2026-03-19 06:33:33', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1305, 1300, '系统设置', 'system_basic_settings', 'C', '/system/settings/basic', 'System/BasicSettings', NULL, 5, 1, 1, 'system:basic-settings:view', '系统设置菜单', 0, '系统初始化', '系统初始化', '2026-03-26 15:34:59', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1306, 1305, '编辑系统设置', 'system_basic_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:basic-settings:edit', '系统设置编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 15:34:59', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1307, 1300, 'APP版本设置', 'system_app_version_settings', 'C', '/system/settings/app-version', 'System/AppVersionSettings', NULL, 15, 1, 1, 'system:app-version:view', 'APP版本设置菜单', 0, '系统初始化', '系统初始化', '2026-03-26 08:13:45', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1308, 1307, '新增APP版本', 'system_app_version_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:app-version:add', '新增APP版本按钮', 0, '系统初始化', '系统初始化', '2026-03-26 08:13:45', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1309, 1307, '查看APP版本', 'system_app_version_settings_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:app-version:detail', '查看APP版本按钮', 0, '系统初始化', '系统初始化', '2026-03-26 08:13:45', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1310, 1300, '积分设置', 'system_points_settings', 'C', '/system/settings/points', 'System/PointsSettings', NULL, 10, 1, 1, 'system:points-settings:view', '积分设置菜单', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1311, 1310, '编辑积分设置', 'system_points_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:points-settings:edit', '积分设置编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1312, 1300, '消息推送配置', 'system_message_push_settings', 'C', '/system/settings/message-push', 'System/MessagePushSettings', NULL, 20, 1, 1, 'system:message-push:view', '消息推送配置菜单', 0, '系统初始化', '系统初始化', '2026-03-26 08:51:05', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1313, 1312, '编辑消息推送配置', 'system_message_push_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:message-push:edit', '消息推送配置编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 08:51:05', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1314, 1300, 'Banner配置', 'system_banner_settings', 'C', '/system/settings/banner', 'System/BannerSettings', NULL, 12, 1, 1, 'system:banner:view', 'Banner配置菜单', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1315, 1314, '新增Banner', 'system_banner_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:banner:add', 'Banner新增按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1316, 1314, '编辑Banner', 'system_banner_settings_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:banner:edit', 'Banner编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1317, 1314, '查看Banner', 'system_banner_settings_detail', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:banner:detail', 'Banner查看按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1318, 1314, '删除Banner', 'system_banner_settings_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:banner:delete', 'Banner删除按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:04');
+INSERT INTO `sys_menu` VALUES (1319, 1314, '批量删除Banner', 'system_banner_settings_batch_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:banner:batch-delete', 'Banner批量删除按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1320, 1314, '切换Banner上架状态', 'system_banner_settings_publish_status', 'B', NULL, NULL, NULL, 6, 1, 1, 'system:banner:publish-status', 'Banner上架状态按钮', 0, '系统初始化', '系统初始化', '2026-03-26 09:28:36', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1321, 1300, '活动通知', 'system_activity_notice', 'C', '/system/settings/activity-notice', 'System/ActivityNotice', NULL, 13, 1, 1, 'system:activity-notice:view', '活动通知菜单', 0, '系统初始化', '系统初始化', '2026-03-26 10:07:49', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1322, 1321, '新增活动通知', 'system_activity_notice_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:activity-notice:add', '活动通知新增按钮', 0, '系统初始化', '系统初始化', '2026-03-26 10:07:49', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1323, 1321, '删除活动通知', 'system_activity_notice_delete', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:activity-notice:delete', '活动通知删除按钮', 0, '系统初始化', '系统初始化', '2026-03-26 10:07:49', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1324, 1300, '平台资质', 'system_platform_qualification', 'C', '/system/settings/platform-qualification', 'System/PlatformQualification', NULL, 14, 1, 1, 'system:platform-qualification:view', '平台资质菜单', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1325, 1324, '新增平台资质', 'system_platform_qualification_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:platform-qualification:add', '平台资质新增按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1326, 1324, '查看平台资质', 'system_platform_qualification_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:platform-qualification:detail', '平台资质详情按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1327, 1324, '编辑平台资质', 'system_platform_qualification_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:platform-qualification:edit', '平台资质编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1328, 1324, '删除平台资质', 'system_platform_qualification_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:platform-qualification:delete', '平台资质删除按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1329, 1324, '切换平台资质上架状态', 'system_platform_qualification_publish_status', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:platform-qualification:publish-status', '平台资质上架状态按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:29:12', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1330, 1300, '协议内容', 'system_agreement_content', 'C', '/system/settings/agreement-content', 'System/AgreementContent', NULL, 16, 1, 1, 'system:agreement-content:view', '协议内容菜单', 0, '系统初始化', '系统初始化', '2026-03-26 11:58:15', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1331, 1330, '编辑协议内容', 'system_agreement_content_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:agreement-content:edit', '协议内容编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 11:58:15', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1332, 1300, '常见问题', 'system_common_question', 'C', '/system/settings/common-question', 'System/CommonQuestion', NULL, 17, 1, 1, 'system:common-question:view', '常见问题菜单', 0, '系统初始化', '系统初始化', '2026-03-26 12:25:39', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1333, 1332, '新增常见问题', 'system_common_question_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:common-question:add', '常见问题新增按钮', 0, '系统初始化', '系统初始化', '2026-03-26 12:25:39', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1334, 1332, '编辑常见问题', 'system_common_question_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:common-question:edit', '常见问题编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-26 12:25:39', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1335, 1332, '删除常见问题', 'system_common_question_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:common-question:delete', '常见问题删除按钮', 0, '系统初始化', '系统初始化', '2026-03-26 12:25:39', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (1336, 1300, '用户意见反馈', 'system_user_feedback', 'C', '/system/settings/user-feedback', 'System/UserFeedback', NULL, 18, 1, 1, 'system:user-feedback:view', '用户意见反馈菜单', 0, '系统初始化', '系统初始化', '2026-03-26 12:43:32', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2000, 0, '用户与会员', 'member_root', 'M', '/member', NULL, 'team', 200, 1, 1, NULL, '用户与会员模块', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2100, 2000, '用户', 'member_user_group', 'M', '/member/user', NULL, NULL, 10, 1, 1, NULL, '用户分组菜单', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2110, 2100, '用户管理', 'member_user_manage', 'C', '/member/user/manage', 'Member/UserList', NULL, 10, 1, 1, 'member:user:view', '用户管理菜单', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2111, 2110, '用户详情', 'member_user_detail', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:user:detail', '用户详情按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2112, 2110, '编辑用户', 'member_user_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'member:user:edit', '编辑用户按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2113, 2110, '启用禁用用户', 'member_user_status', 'B', NULL, NULL, NULL, 3, 1, 1, 'member:user:status', '启用禁用用户按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2114, 2110, '重置用户密码', 'member_user_reset_password', 'B', NULL, NULL, NULL, 4, 1, 1, 'member:user:reset-password', '重置用户密码按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2115, 2110, '删除用户', 'member_user_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'member:user:delete', '删除用户按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2116, 2110, '导出用户', 'member_user_export', 'B', NULL, NULL, NULL, 6, 1, 1, 'member:user:export', '导出用户按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2117, 2110, '解绑微信', 'member_user_unbind_wechat', 'B', NULL, NULL, NULL, 7, 1, 1, 'member:user:unbind-wechat', '解绑微信按钮', 0, '系统初始化', '系统初始化', '2026-03-20 06:36:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2118, 2100, '诊所管理', 'member_clinic_manage', 'C', '/member/user/clinic', 'Member/ClinicList', NULL, 11, 1, 1, 'member:clinic:view', '诊所管理菜单', 0, '系统初始化', '系统初始化', '2026-03-23 19:49:56', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2119, 2100, '会员用户', 'member_only_user_manage', 'C', '/member/user/member-users', 'Member/MemberUserList', NULL, 12, 1, 1, 'member:user:member:view', '会员用户菜单', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2200, 2000, '会员', 'member_vip_group', 'M', '/member/vip', NULL, NULL, 20, 1, 1, NULL, '会员分组菜单', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2210, 2200, '会员价格设置', 'member_vip_price_settings', 'C', '/member/vip/price', 'Member/VipPriceSettings', NULL, 10, 1, 1, 'member:vip-price:view', '会员价格设置菜单', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2211, 2210, '编辑会员价格设置', 'member_vip_price_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:vip-price:edit', '会员价格设置编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2220, 2200, '会员介绍设置', 'member_vip_intro_settings', 'C', '/member/vip/intro', 'Member/VipIntroSettings', NULL, 20, 1, 1, 'member:vip-intro:view', '会员介绍设置菜单', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (2221, 2220, '编辑会员介绍设置', 'member_vip_intro_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:vip-intro:edit', '会员介绍设置编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-23 22:00:08', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3000, 0, '课程与学习', 'live_root', 'M', '/live', NULL, 'read', 300, 1, 1, NULL, '课程与学习模块', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3050, 3000, '课程管理', 'live_course_manage_root', 'M', '/live/course', NULL, NULL, 10, 1, 1, NULL, '课程管理目录', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3060, 3050, '养生直播', 'live_course_broadcast_manage', 'C', '/live/course/broadcast', 'Live/BroadcastList', NULL, 10, 1, 1, 'live:course:view', '养生直播菜单', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3061, 3060, '直播详情', 'live_course_detail', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:course:detail', '养生直播详情按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3062, 3060, '编辑直播', 'live_course_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:course:edit', '养生直播编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3063, 3060, '启用禁用直播', 'live_course_status', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:course:status', '养生直播状态按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3064, 3060, '设置直播间密码', 'live_course_password', 'B', NULL, NULL, NULL, 4, 1, 1, 'live:course:password', '设置直播间密码按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3070, 3060, '场次管理', 'live_session_manage', 'B', NULL, NULL, NULL, 10, 1, 1, 'live:session:detail', '直播场次管理权限', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3071, 3060, '新增场次', 'live_session_add', 'B', NULL, NULL, NULL, 11, 1, 1, 'live:session:add', '直播场次新增按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3072, 3060, '编辑场次', 'live_session_edit', 'B', NULL, NULL, NULL, 12, 1, 1, 'live:session:edit', '直播场次编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3073, 3060, '开播场次', 'live_session_start', 'B', NULL, NULL, NULL, 13, 1, 1, 'live:session:start', '直播场次开播按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3074, 3060, '结束场次', 'live_session_end', 'B', NULL, NULL, NULL, 14, 1, 1, 'live:session:end', '直播场次结束按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3075, 3060, '复制场次', 'live_session_copy', 'B', NULL, NULL, NULL, 15, 1, 1, 'live:session:copy', '直播场次复制按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3076, 3060, '删除场次', 'live_session_delete', 'B', NULL, NULL, NULL, 16, 1, 1, 'live:session:delete', '直播场次删除按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3077, 3060, '推送开播提醒', 'live_session_push_notification', 'B', NULL, NULL, NULL, 17, 1, 1, 'live:session:push-notification', '直播场次推送提醒按钮', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3080, 3050, '直播预约记录', 'live_reserve_view', 'C', '/live/course/reserve-records', 'Live/ReserveRecords', NULL, 20, 1, 1, 'live:reserve:view', '直播预约记录菜单', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3081, 3050, '直播观看记录', 'live_watch_view', 'C', '/live/course/watch-records', 'Live/WatchRecords', NULL, 21, 1, 1, 'live:watch:view', '直播观看记录菜单', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3082, 3060, '答题记录', 'live_quiz_view', 'B', NULL, NULL, NULL, 22, 1, 1, 'live:quiz:view', '直播答题记录查看权限', 0, '系统初始化', '系统初始化', '2026-03-20 09:58:00', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3090, 3050, '分类管理', 'live_category_manage', 'C', '/live/course/category', 'Live/CategoryList', NULL, 22, 1, 1, 'live:category:view', '分类管理菜单', 0, '系统初始化', '系统初始化', '2026-03-24 10:59:54', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3091, 3090, '新增分类', 'live_category_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:category:add', '分类新增按钮', 0, '系统初始化', '系统初始化', '2026-03-24 10:59:54', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3092, 3090, '编辑分类', 'live_category_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:category:edit', '分类编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-24 10:59:54', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3093, 3090, '删除分类', 'live_category_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:category:delete', '分类删除按钮', 0, '系统初始化', '系统初始化', '2026-03-24 10:59:54', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3100, 3050, '授课专家管理', 'live_expert_manage', 'C', '/live/course/expert', 'Live/ExpertList', NULL, 30, 1, 1, 'live:expert:view', '授课专家管理菜单', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3101, 3100, '新增授课专家', 'live_expert_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:expert:add', '授课专家新增按钮', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3102, 3100, '编辑授课专家', 'live_expert_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:expert:edit', '授课专家编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3103, 3100, '启用禁用授课专家', 'live_expert_status', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:expert:status', '授课专家状态按钮', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3104, 3100, '删除授课专家', 'live_expert_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'live:expert:delete', '授课专家删除按钮', 0, '系统初始化', '系统初始化', '2026-03-20 08:03:03', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3110, 3050, '养生课程', 'live_health_course_manage', 'C', '/live/course/health', 'Live/HealthCourseList', NULL, 40, 1, 1, 'live:health-course:view', '养生课程菜单', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3111, 3110, '养生课程详情', 'live_health_course_detail', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:health-course:detail', '养生课程详情按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3112, 3110, '新增养生课程', 'live_health_course_add', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:health-course:add', '养生课程新增按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3113, 3110, '编辑养生课程', 'live_health_course_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:health-course:edit', '养生课程编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3114, 3110, '删除养生课程', 'live_health_course_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'live:health-course:delete', '养生课程删除按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (3115, 3110, '启用禁用养生课程', 'live_health_course_status', 'B', NULL, NULL, NULL, 5, 1, 1, 'live:health-course:status', '养生课程状态按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3116, 3110, '导出养生课程', 'live_health_course_export', 'B', NULL, NULL, NULL, 6, 1, 1, 'live:health-course:export', '养生课程导出按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3117, 3110, '新增视频课程', 'live_health_course_video_add', 'B', NULL, NULL, NULL, 7, 1, 1, 'live:health-course:video:add', '养生课程视频新增按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3118, 3110, '编辑视频课程', 'live_health_course_video_edit', 'B', NULL, NULL, NULL, 8, 1, 1, 'live:health-course:video:edit', '养生课程视频编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3119, 3110, '删除视频课程', 'live_health_course_video_delete', 'B', NULL, NULL, NULL, 9, 1, 1, 'live:health-course:video:delete', '养生课程视频删除按钮', 0, '系统初始化', '系统初始化', '2026-03-24 05:50:17', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3120, 6100, '健康知识', 'live_health_popularization_manage', 'C', '/live/promotion/health-popularization', 'Live/HealthPopularizationList', NULL, 50, 1, 1, 'live:health-popularization:view', '健康知识菜单', 0, '系统初始化', '系统初始化', '2026-03-25 13:30:33', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3121, 3120, '新增健康知识', 'live_health_popularization_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:health-popularization:add', '健康知识新增按钮', 0, '系统初始化', '系统初始化', '2026-03-25 13:30:33', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3122, 3120, '编辑健康知识', 'live_health_popularization_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:health-popularization:edit', '健康知识编辑按钮', 0, '系统初始化', '系统初始化', '2026-03-25 13:30:33', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (3123, 3120, '删除健康知识', 'live_health_popularization_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:health-popularization:delete', '健康知识删除按钮', 0, '系统初始化', '系统初始化', '2026-03-25 13:30:33', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4000, 0, '交易与财务', 'trade_root', 'M', '/trade', NULL, 'moneyCollect', 400, 1, 1, NULL, '交易与财务模块', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4100, 4000, '订单', 'trade_order_group', 'M', '/trade/order', NULL, NULL, 10, 1, 1, NULL, '订单目录', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4110, 4100, '积分兑换', 'trade_points_exchange', 'C', '/trade/order/points-exchange', 'Trade/PointsExchange', NULL, 10, 1, 1, 'points:exchange:view', '积分兑换菜单', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4111, 4110, '兑换详情', 'trade_points_exchange_detail', 'B', NULL, NULL, NULL, 1, 1, 1, 'points:exchange:detail', '积分兑换详情按钮', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4112, 4110, '积分兑换发货', 'trade_points_exchange_ship', 'B', NULL, NULL, NULL, 2, 1, 1, 'points:exchange:ship', '积分兑换发货按钮', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4113, 4110, '积分兑换删除', 'trade_points_exchange_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'points:exchange:delete', '积分兑换删除按钮', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4114, 4110, '积分兑换导出', 'trade_points_exchange_export', 'B', NULL, NULL, NULL, 4, 1, 1, 'points:exchange:export', '积分兑换导出按钮', 0, '系统初始化', '系统初始化', '2026-03-21 08:07:45', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4120, 4100, '会员购买订单', 'trade_member_purchase_order', 'C', '/trade/order/member-purchase', 'Trade/MemberPurchase', NULL, 9, 1, 1, 'member:purchase-order:view', '会员购买订单菜单', 0, '系统初始化', '系统初始化', '2026-03-25 14:59:13', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4121, 4120, '导出会员购买订单', 'trade_member_purchase_order_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:purchase-order:export', '会员购买订单导出按钮', 0, '系统初始化', '系统初始化', '2026-03-25 14:59:13', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4122, 4120, '退款会员购买订单', 'trade_member_purchase_order_refund', 'B', NULL, NULL, NULL, 2, 1, 1, 'member:purchase-order:refund', '会员购买订单退款按钮', 0, '系统初始化', '系统初始化', '2026-03-25 14:59:13', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4123, 4120, '删除会员购买订单', 'trade_member_purchase_order_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'member:purchase-order:delete', '会员购买订单删除按钮', 0, '系统初始化', '系统初始化', '2026-03-25 14:59:13', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4130, 4100, '养生课程购买订单', 'trade_health_course_purchase_order', 'C', '/trade/order/course-purchase', 'Trade/CoursePurchase', NULL, 8, 1, 1, 'course:purchase-order:view', '养生课程购买订单菜单', 0, '系统初始化', '系统初始化', '2026-03-25 16:18:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4131, 4130, '导出养生课程购买订单', 'trade_health_course_purchase_order_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'course:purchase-order:export', '养生课程购买订单导出按钮', 0, '系统初始化', '系统初始化', '2026-03-25 16:18:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4132, 4130, '退款养生课程购买订单', 'trade_health_course_purchase_order_refund', 'B', NULL, NULL, NULL, 2, 1, 1, 'course:purchase-order:refund', '养生课程购买订单退款按钮', 0, '系统初始化', '系统初始化', '2026-03-25 16:18:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4133, 4130, '删除养生课程购买订单', 'trade_health_course_purchase_order_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'course:purchase-order:delete', '养生课程购买订单删除按钮', 0, '系统初始化', '系统初始化', '2026-03-25 16:18:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4200, 4000, '财务流水', 'trade_finance_flow_group', 'M', '/trade/flow', NULL, NULL, 20, 1, 1, NULL, '财务流水目录', 0, '系统初始化', '系统初始化', '2026-03-21 09:30:56', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4210, 4200, '积分收支明细', 'trade_points_ledger', 'C', '/trade/flow/points-ledger', 'Trade/PointsLedger', NULL, 30, 1, 1, 'points:ledger:view', '积分收支明细菜单', 0, '系统初始化', '系统初始化', '2026-03-21 09:30:56', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4211, 4210, '导出积分收支明细', 'trade_points_ledger_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'points:ledger:export', '积分收支明细导出按钮', 0, '系统初始化', '系统初始化', '2026-03-21 09:30:56', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4220, 4200, '支付流水', 'trade_payment_flow', 'C', '/trade/flow/payment-flow', 'Trade/PaymentFlow', NULL, 10, 1, 1, 'payment:flow:view', '支付流水菜单', 0, '系统初始化', '系统初始化', '2026-03-25 09:40:43', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4221, 4220, '导出支付流水', 'trade_payment_flow_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'payment:flow:export', '支付流水导出按钮', 0, '系统初始化', '系统初始化', '2026-03-25 09:40:43', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4230, 4200, '退费流水', 'trade_refund_flow', 'C', '/trade/flow/refund-flow', 'Trade/RefundFlow', NULL, 20, 1, 1, 'refund:flow:view', '退费流水菜单', 0, '系统初始化', '系统初始化', '2026-03-25 20:51:18', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (4231, 4230, '导出退费流水', 'trade_refund_flow_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'refund:flow:export', '退费流水导出按钮', 0, '系统初始化', '系统初始化', '2026-03-25 20:51:18', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (5000, 0, '统计', 'statistics_root', 'M', '/statistics', NULL, 'barChart', 150, 1, 1, NULL, '统计模块', 0, '系统初始化', '系统初始化', '2026-03-26 21:07:19', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5010, 5000, '用户数据', 'statistics_user_data', 'C', '/statistics/user-data', 'Statistics/UserData', NULL, 10, 1, 1, 'statistics:user-data:view', '用户数据菜单', 0, '系统初始化', '系统初始化', '2026-03-26 13:56:30', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5020, 5000, '内容数据', 'statistics_content_data', 'C', '/statistics/content-data', 'Statistics/ContentData', NULL, 20, 1, 1, 'statistics:content-data:view', '内容数据菜单', 0, '系统初始化', '系统初始化', '2026-03-26 23:06:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5030, 5000, '营收数据', 'statistics_revenue', 'C', '/statistics/revenue', 'Statistics/Revenue', NULL, 30, 1, 1, 'statistics:revenue:view', '营收数据菜单', 0, '系统初始化', '系统初始化', '2026-03-26 21:07:19', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5031, 5030, '导出营收报表', 'statistics_revenue_export', 'B', NULL, NULL, NULL, 1, 1, 1, 'statistics:revenue:export', '营收数据导出按钮', 0, '系统初始化', '系统初始化', '2026-03-26 21:07:19', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5045, 2118, '编辑', 'member_clinic_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:clinic:edit', '编辑诊所按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:30', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5046, 2100, '省代理管理', 'member_agent_manage', 'C', '/member/user/agent', 'Member/AgentList', NULL, 12, 1, 1, 'member:agent:view', '省代理管理菜单', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5047, 5046, '新增省代理', 'member_agent_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'member:agent:add', '新增省代理按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5048, 5046, '编辑省代理', 'member_agent_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'member:agent:edit', '编辑省代理按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5049, 5046, '删除省代理', 'member_agent_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'member:agent:delete', '删除省代理按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (5050, 5046, '省代理详情', 'member_agent_detail', 'B', NULL, NULL, NULL, 4, 1, 1, 'member:agent:detail', '省代理详情按钮', 0, '系统初始化', '系统初始化', '2026-08-19 10:27:29', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (6000, 3050, '微赞直播间设置', 'live_room_setting', 'C', '/live/course/live-room-setting', 'Live/LiveRoomSetting', NULL, 18, 1, 1, 'live:room-setting:view', '微赞直播间设置菜单', 0, '系统初始化', '系统初始化', '2026-08-19 16:22:09', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (6001, 6000, '编辑微赞直播间设置', 'live_room_setting_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:room-setting:edit', '微赞直播间设置编辑按钮', 0, '系统初始化', '系统初始化', '2026-08-19 16:22:09', '2026-08-24 10:14:05');
+INSERT INTO `sys_menu` VALUES (6010, 6100, '项目介绍', 'live_project_introduction_manage', 'C', '/live/promotion/project-introduction', 'Live/ProjectIntroductionList', NULL, 60, 1, 1, 'live:project-introduction:view', '项目介绍菜单', 0, '系统初始化', '系统初始化', '2026-08-20 12:10:14', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6011, 6010, '新增项目介绍', 'live_project_introduction_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:project-introduction:add', '项目介绍新增按钮', 0, '系统初始化', '系统初始化', '2026-08-20 12:10:14', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6012, 6010, '编辑项目介绍', 'live_project_introduction_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:project-introduction:edit', '项目介绍编辑按钮', 0, '系统初始化', '系统初始化', '2026-08-20 12:10:14', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6013, 6010, '删除项目介绍', 'live_project_introduction_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:project-introduction:delete', '项目介绍删除按钮', 0, '系统初始化', '系统初始化', '2026-08-20 12:10:14', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6014, 6100, '项目介绍密码', 'live_project_introduction_password', 'C', '/live/promotion/project-introduction-password', 'Live/ProjectIntroductionPassword', NULL, 61, 1, 1, 'live:project-introduction-password:view', '项目介绍密码菜单', 0, '系统初始化', '系统初始化', '2026-08-20 13:05:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6015, 6014, '编辑项目介绍密码', 'live_project_introduction_password_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:project-introduction-password:edit', '项目介绍密码编辑按钮', 0, '系统初始化', '系统初始化', '2026-08-20 13:05:55', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6100, 3000, '宣传知识', 'live_promotion_root', 'M', '/live/promotion', NULL, NULL, 20, 1, 1, NULL, '宣传知识目录', 0, '系统初始化', '系统初始化', '2026-08-22 11:09:24', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6110, 6100, '养生科普', 'live_health_promotion_manage', 'C', '/live/promotion/health-promotion', 'Live/HealthPromotionList', NULL, 51, 1, 1, 'live:health-promotion:view', '养生科普菜单', 0, '系统初始化', '系统初始化', '2026-08-22 12:12:19', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6111, 6110, '新增养生科普', 'live_health_promotion_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:health-promotion:add', '养生科普新增按钮', 0, '系统初始化', '系统初始化', '2026-08-22 12:12:19', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6112, 6110, '编辑养生科普', 'live_health_promotion_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:health-promotion:edit', '养生科普编辑按钮', 0, '系统初始化', '系统初始化', '2026-08-22 12:12:19', '2026-08-24 10:14:06');
+INSERT INTO `sys_menu` VALUES (6113, 6110, '删除养生科普', 'live_health_promotion_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:health-promotion:delete', '养生科普删除按钮', 0, '系统初始化', '系统初始化', '2026-08-22 12:12:19', '2026-08-24 10:14:06');
+
+-- ----------------------------
+-- Table structure for sys_role_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_menu`;
+CREATE TABLE `sys_role_menu` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `role_id` bigint NOT NULL COMMENT '角色ID',
+ `menu_id` bigint NOT NULL COMMENT '菜单/权限ID',
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_sys_role_menu`(`role_id` ASC, `menu_id` ASC) USING BTREE,
+ INDEX `idx_sys_role_menu_menu_id`(`menu_id` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2429 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色菜单权限关联表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_role_menu
+-- ----------------------------
+INSERT INTO `sys_role_menu` VALUES (35, 2, 1000, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (36, 2, 1100, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (37, 2, 1101, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (38, 2, 1102, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (39, 2, 1103, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (40, 2, 1104, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (41, 2, 1105, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (42, 2, 1200, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (43, 2, 1201, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (44, 2, 1202, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (45, 2, 1203, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (46, 2, 1204, '2026-03-19 15:15:13');
+INSERT INTO `sys_role_menu` VALUES (66, 1, 1100, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (67, 1, 1101, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (68, 1, 1102, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (69, 1, 1103, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (70, 1, 1104, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (71, 1, 1105, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (72, 1, 1200, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (73, 1, 1201, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (74, 1, 1202, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (75, 1, 1203, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (76, 1, 1204, '2026-03-19 15:47:23');
+INSERT INTO `sys_role_menu` VALUES (89, 1, 1000, '2026-03-19 16:13:15');
+INSERT INTO `sys_role_menu` VALUES (90, 4, 1100, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (91, 4, 1101, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (92, 4, 1102, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (93, 4, 1103, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (94, 4, 1104, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (95, 4, 1105, '2026-03-19 16:41:05');
+INSERT INTO `sys_role_menu` VALUES (108, 1, 2000, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (109, 1, 2100, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (110, 1, 2110, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (111, 1, 2111, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (112, 1, 2112, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (113, 1, 2113, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (114, 1, 2114, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (115, 1, 2115, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (116, 1, 2116, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (117, 1, 2117, '2026-03-20 06:36:03');
+INSERT INTO `sys_role_menu` VALUES (240, 1, 3000, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (241, 1, 3100, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (242, 1, 3101, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (243, 1, 3102, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (244, 1, 3103, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (245, 1, 3104, '2026-03-20 08:03:03');
+INSERT INTO `sys_role_menu` VALUES (268, 1, 3050, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (269, 1, 3060, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (270, 1, 3061, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (271, 1, 3062, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (272, 1, 3063, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (273, 1, 3070, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (274, 1, 3071, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (275, 1, 3072, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (276, 1, 3073, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (277, 1, 3074, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (278, 1, 3075, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (279, 1, 3076, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (280, 1, 3077, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (281, 1, 3080, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (282, 1, 3081, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (283, 1, 3082, '2026-03-20 09:58:00');
+INSERT INTO `sys_role_menu` VALUES (358, 4, 1300, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (359, 2, 1300, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (360, 1, 1300, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (361, 4, 1310, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (362, 2, 1310, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (363, 1, 1310, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (364, 4, 1311, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (365, 2, 1311, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (366, 1, 1311, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (367, 4, 4000, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (368, 2, 4000, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (369, 1, 4000, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (370, 4, 4100, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (371, 2, 4100, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (372, 1, 4100, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (373, 4, 4110, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (374, 2, 4110, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (375, 1, 4110, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (376, 4, 4111, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (377, 2, 4111, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (378, 1, 4111, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (379, 4, 4112, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (380, 2, 4112, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (381, 1, 4112, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (382, 4, 4113, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (383, 2, 4113, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (384, 1, 4113, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (385, 4, 4114, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (386, 2, 4114, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (387, 1, 4114, '2026-03-21 08:07:45');
+INSERT INTO `sys_role_menu` VALUES (433, 4, 4200, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (434, 2, 4200, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (435, 1, 4200, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (436, 4, 4210, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (437, 2, 4210, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (438, 1, 4210, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (439, 4, 4211, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (440, 2, 4211, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (441, 1, 4211, '2026-03-21 09:30:56');
+INSERT INTO `sys_role_menu` VALUES (451, 1, 2118, '2026-03-23 19:49:56');
+INSERT INTO `sys_role_menu` VALUES (452, 1, 2119, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (453, 1, 2200, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (454, 1, 2210, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (455, 1, 2211, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (456, 1, 2220, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (457, 1, 2221, '2026-03-23 22:00:08');
+INSERT INTO `sys_role_menu` VALUES (458, 1, 3090, '2026-03-24 10:59:54');
+INSERT INTO `sys_role_menu` VALUES (459, 1, 3091, '2026-03-24 10:59:54');
+INSERT INTO `sys_role_menu` VALUES (460, 1, 3092, '2026-03-24 10:59:54');
+INSERT INTO `sys_role_menu` VALUES (461, 1, 3093, '2026-03-24 10:59:54');
+INSERT INTO `sys_role_menu` VALUES (530, 1, 3110, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (531, 1, 3111, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (532, 1, 3112, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (533, 1, 3113, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (534, 1, 3114, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (535, 1, 3115, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (536, 1, 3116, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (537, 1, 3117, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (538, 1, 3118, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (539, 1, 3119, '2026-03-24 05:50:17');
+INSERT INTO `sys_role_menu` VALUES (608, 1, 4120, '2026-03-25 14:59:13');
+INSERT INTO `sys_role_menu` VALUES (609, 1, 4121, '2026-03-25 14:59:13');
+INSERT INTO `sys_role_menu` VALUES (610, 1, 4122, '2026-03-25 14:59:13');
+INSERT INTO `sys_role_menu` VALUES (611, 1, 4123, '2026-03-25 14:59:13');
+INSERT INTO `sys_role_menu` VALUES (776, 1, 4130, '2026-03-25 16:18:55');
+INSERT INTO `sys_role_menu` VALUES (777, 1, 4131, '2026-03-25 16:18:55');
+INSERT INTO `sys_role_menu` VALUES (778, 1, 4132, '2026-03-25 16:18:55');
+INSERT INTO `sys_role_menu` VALUES (779, 1, 4133, '2026-03-25 16:18:55');
+INSERT INTO `sys_role_menu` VALUES (780, 1, 4220, '2026-03-25 09:40:43');
+INSERT INTO `sys_role_menu` VALUES (781, 1, 4221, '2026-03-25 09:40:43');
+INSERT INTO `sys_role_menu` VALUES (868, 1, 4230, '2026-03-25 20:51:18');
+INSERT INTO `sys_role_menu` VALUES (869, 1, 4231, '2026-03-25 20:51:18');
+INSERT INTO `sys_role_menu` VALUES (870, 1, 3120, '2026-03-25 13:30:33');
+INSERT INTO `sys_role_menu` VALUES (871, 1, 3121, '2026-03-25 13:30:33');
+INSERT INTO `sys_role_menu` VALUES (872, 1, 3122, '2026-03-25 13:30:33');
+INSERT INTO `sys_role_menu` VALUES (873, 1, 3123, '2026-03-25 13:30:33');
+INSERT INTO `sys_role_menu` VALUES (964, 1, 1305, '2026-03-26 15:34:59');
+INSERT INTO `sys_role_menu` VALUES (965, 1, 1306, '2026-03-26 15:34:59');
+INSERT INTO `sys_role_menu` VALUES (1062, 1, 1307, '2026-03-26 08:13:45');
+INSERT INTO `sys_role_menu` VALUES (1063, 1, 1308, '2026-03-26 08:13:45');
+INSERT INTO `sys_role_menu` VALUES (1064, 1, 1309, '2026-03-26 08:13:45');
+INSERT INTO `sys_role_menu` VALUES (1161, 1, 1312, '2026-03-26 08:51:05');
+INSERT INTO `sys_role_menu` VALUES (1162, 1, 1313, '2026-03-26 08:51:05');
+INSERT INTO `sys_role_menu` VALUES (1262, 1, 1314, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1263, 1, 1315, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1264, 1, 1316, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1265, 1, 1317, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1266, 1, 1318, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1267, 1, 1319, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1268, 1, 1320, '2026-03-26 09:28:36');
+INSERT INTO `sys_role_menu` VALUES (1370, 1, 1321, '2026-03-26 10:07:49');
+INSERT INTO `sys_role_menu` VALUES (1371, 1, 1322, '2026-03-26 10:07:49');
+INSERT INTO `sys_role_menu` VALUES (1372, 1, 1323, '2026-03-26 10:07:49');
+INSERT INTO `sys_role_menu` VALUES (1481, 1, 1324, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1482, 1, 1325, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1483, 1, 1326, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1484, 1, 1327, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1485, 1, 1328, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1486, 1, 1329, '2026-03-26 11:29:12');
+INSERT INTO `sys_role_menu` VALUES (1598, 1, 1330, '2026-03-26 11:58:15');
+INSERT INTO `sys_role_menu` VALUES (1599, 1, 1331, '2026-03-26 11:58:15');
+INSERT INTO `sys_role_menu` VALUES (1727, 1, 1332, '2026-03-26 12:25:39');
+INSERT INTO `sys_role_menu` VALUES (1728, 1, 1333, '2026-03-26 12:25:39');
+INSERT INTO `sys_role_menu` VALUES (1729, 1, 1334, '2026-03-26 12:25:39');
+INSERT INTO `sys_role_menu` VALUES (1730, 1, 1335, '2026-03-26 12:25:39');
+INSERT INTO `sys_role_menu` VALUES (1855, 1, 1336, '2026-03-26 12:43:32');
+INSERT INTO `sys_role_menu` VALUES (1981, 1, 5000, '2026-03-26 21:07:20');
+INSERT INTO `sys_role_menu` VALUES (1982, 1, 5030, '2026-03-26 21:07:20');
+INSERT INTO `sys_role_menu` VALUES (1983, 1, 5031, '2026-03-26 21:07:20');
+INSERT INTO `sys_role_menu` VALUES (2111, 1, 5010, '2026-03-26 21:57:36');
+INSERT INTO `sys_role_menu` VALUES (2112, 1, 5020, '2026-03-26 23:06:29');
+INSERT INTO `sys_role_menu` VALUES (2367, 5, 1312, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2368, 5, 1313, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2369, 5, 1336, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2370, 5, 2110, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2371, 5, 2111, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2372, 5, 2112, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2373, 5, 2113, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2374, 5, 2114, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2375, 5, 2115, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2376, 5, 2116, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2377, 5, 2117, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2378, 5, 2118, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2379, 5, 2119, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2380, 5, 2210, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2381, 5, 2211, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2382, 5, 2220, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2383, 5, 2221, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2384, 5, 3080, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2385, 5, 3081, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2386, 5, 4110, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2387, 5, 4111, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2388, 5, 4112, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2389, 5, 4113, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2390, 5, 4114, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2391, 5, 4120, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2392, 5, 4121, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2393, 5, 4122, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2394, 5, 4123, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2395, 5, 4130, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2396, 5, 4131, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2397, 5, 4132, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2398, 5, 4133, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2399, 5, 4210, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2400, 5, 4211, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2401, 5, 4230, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2402, 5, 4231, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2403, 5, 5010, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2404, 5, 5020, '2026-04-28 10:24:25');
+INSERT INTO `sys_role_menu` VALUES (2405, 1, 5045, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2406, 1, 5046, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2407, 1, 5047, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2408, 1, 5048, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2409, 1, 5049, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2410, 1, 5050, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2411, 1, 3064, '2026-08-19 10:38:59');
+INSERT INTO `sys_role_menu` VALUES (2416, 1, 6000, '2026-08-19 16:22:28');
+INSERT INTO `sys_role_menu` VALUES (2417, 1, 6001, '2026-08-19 16:22:28');
+INSERT INTO `sys_role_menu` VALUES (2418, 1, 6010, '2026-08-20 12:10:14');
+INSERT INTO `sys_role_menu` VALUES (2419, 1, 6011, '2026-08-20 12:10:14');
+INSERT INTO `sys_role_menu` VALUES (2420, 1, 6012, '2026-08-20 12:10:14');
+INSERT INTO `sys_role_menu` VALUES (2421, 1, 6013, '2026-08-20 12:10:14');
+INSERT INTO `sys_role_menu` VALUES (2422, 1, 6014, '2026-08-20 13:05:56');
+INSERT INTO `sys_role_menu` VALUES (2423, 1, 6015, '2026-08-20 13:05:56');
+INSERT INTO `sys_role_menu` VALUES (2424, 1, 6100, '2026-08-22 11:09:25');
+INSERT INTO `sys_role_menu` VALUES (2425, 1, 6110, '2026-08-22 12:12:19');
+INSERT INTO `sys_role_menu` VALUES (2426, 1, 6111, '2026-08-22 12:12:19');
+INSERT INTO `sys_role_menu` VALUES (2427, 1, 6112, '2026-08-22 12:12:19');
+INSERT INTO `sys_role_menu` VALUES (2428, 1, 6113, '2026-08-22 12:12:19');
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/controller/AppHealthPopularizationController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/controller/AppHealthPopularizationController.java
index 9769c59..a11f045 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/controller/AppHealthPopularizationController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/controller/AppHealthPopularizationController.java
@@ -8,6 +8,7 @@
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 java.util.List;
@@ -31,10 +32,13 @@
/**
* 获取 APP 健康科普列表。
+ *
+ * @param contentType 内容类型:1 健康知识、2 养生科普;不传则返回全部。
*/
@GetMapping("/health-popularizations")
- public ApiResponse<List<AppHealthPopularizationListItemVO>> list() {
- return ApiResponse.ok(appHealthPopularizationService.list());
+ public ApiResponse<List<AppHealthPopularizationListItemVO>> list(
+ @RequestParam(value = "contentType", required = false) Integer contentType) {
+ return ApiResponse.ok(appHealthPopularizationService.list(contentType));
}
/**
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/service/AppHealthPopularizationService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/service/AppHealthPopularizationService.java
index f4436bf..ec784cd 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/service/AppHealthPopularizationService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/service/AppHealthPopularizationService.java
@@ -6,6 +6,7 @@
import com.tcm.training.admin.live.entity.HealthPopularization;
import com.tcm.training.admin.live.mapper.HealthPopularizationMapper;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
@@ -30,12 +31,15 @@
/**
* 获取 APP 健康科普列表页数据。
+ *
+ * @param contentType 内容类型:1 健康知识、2 养生科普;为空则返回全部。
*/
- public List<AppHealthPopularizationListItemVO> list() {
+ public List<AppHealthPopularizationListItemVO> list(Integer contentType) {
List<HealthPopularization> items = healthPopularizationMapper.selectList(new LambdaQueryWrapper<HealthPopularization>()
.eq(HealthPopularization::getDeleted, 0)
.eq(HealthPopularization::getStatus, STATUS_ENABLED)
- .orderByAsc(HealthPopularization::getSort)
+ .eq(contentType != null, HealthPopularization::getContentType, contentType)
+ .orderByDesc(HealthPopularization::getSort)
.orderByDesc(HealthPopularization::getId));
if (items == null || items.isEmpty()) {
return Collections.emptyList();
@@ -55,10 +59,14 @@
}
/**
- * 获取 APP 健康科普详情页数据。
+ * 获取 APP 健康科普详情页数据,并在每次访问时累加点击量。
*/
+ @Transactional
public AppHealthPopularizationDetailVO detail(Long popularizationId) {
HealthPopularization item = requireEnabledItem(popularizationId);
+ // 参考项目介绍,APP 进入详情后累加点击量并回写。
+ item.setClickCount(defaultNumber(item.getClickCount()) + 1);
+ healthPopularizationMapper.updateById(item);
AppHealthPopularizationDetailVO detailVO = new AppHealthPopularizationDetailVO();
detailVO.setId(item.getId());
detailVO.setTitle(trimToEmpty(item.getTitle()));
@@ -68,6 +76,8 @@
detailVO.setContent(trimToEmpty(item.getContent()));
detailVO.setVideoUrl(trimToEmpty(item.getVideoUrl()));
detailVO.setVideoFileName(trimToEmpty(item.getVideoFileName()));
+ detailVO.setVideoDescription(trimToEmpty(item.getVideoDescription()));
+ detailVO.setClickCount(defaultNumber(item.getClickCount()));
return detailVO;
}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/vo/AppHealthPopularizationDetailVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/vo/AppHealthPopularizationDetailVO.java
index 556e4ef..d62a011 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/vo/AppHealthPopularizationDetailVO.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/popularization/vo/AppHealthPopularizationDetailVO.java
@@ -25,4 +25,8 @@
private String videoUrl;
private String videoFileName;
+
+ private String videoDescription;
+
+ private Integer clickCount;
}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
index 144d0fa..eb01d3a 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -316,24 +316,34 @@
null, null, null, 8, "live:health-course:video:edit", "养生课程视频编辑按钮"));
menuIds.add(saveMenu(3119L, 3110L, "删除视频课程", "live_health_course_video_delete", "B",
null, null, null, 9, "live:health-course:video:delete", "养生课程视频删除按钮"));
- menuIds.add(saveMenu(3120L, 3050L, "健康科普", "live_health_popularization_manage", "C",
- "/live/course/health-popularization", "Live/HealthPopularizationList", null, 50, "live:health-popularization:view", "健康科普菜单"));
- menuIds.add(saveMenu(3121L, 3120L, "新增健康科普", "live_health_popularization_add", "B",
- null, null, null, 1, "live:health-popularization:add", "健康科普新增按钮"));
- menuIds.add(saveMenu(3122L, 3120L, "编辑健康科普", "live_health_popularization_edit", "B",
- null, null, null, 2, "live:health-popularization:edit", "健康科普编辑按钮"));
- menuIds.add(saveMenu(3123L, 3120L, "删除健康科普", "live_health_popularization_delete", "B",
- null, null, null, 3, "live:health-popularization:delete", "健康科普删除按钮"));
- menuIds.add(saveMenu(6010L, 3050L, "项目介绍", "live_project_introduction_manage", "C",
- "/live/course/project-introduction", "Live/ProjectIntroductionList", null, 60, "live:project-introduction:view", "项目介绍菜单"));
+ menuIds.add(saveMenu(3120L, 6100L, "健康知识", "live_health_popularization_manage", "C",
+ "/live/promotion/health-popularization", "Live/HealthPopularizationList", null, 50, "live:health-popularization:view", "健康知识菜单"));
+ menuIds.add(saveMenu(3121L, 3120L, "新增健康知识", "live_health_popularization_add", "B",
+ null, null, null, 1, "live:health-popularization:add", "健康知识新增按钮"));
+ menuIds.add(saveMenu(3122L, 3120L, "编辑健康知识", "live_health_popularization_edit", "B",
+ null, null, null, 2, "live:health-popularization:edit", "健康知识编辑按钮"));
+ menuIds.add(saveMenu(3123L, 3120L, "删除健康知识", "live_health_popularization_delete", "B",
+ null, null, null, 3, "live:health-popularization:delete", "健康知识删除按钮"));
+ menuIds.add(saveMenu(6110L, 6100L, "养生科普", "live_health_promotion_manage", "C",
+ "/live/promotion/health-promotion", "Live/HealthPromotionList", null, 51, "live:health-promotion:view", "养生科普菜单"));
+ menuIds.add(saveMenu(6111L, 6110L, "新增养生科普", "live_health_promotion_add", "B",
+ null, null, null, 1, "live:health-promotion:add", "养生科普新增按钮"));
+ menuIds.add(saveMenu(6112L, 6110L, "编辑养生科普", "live_health_promotion_edit", "B",
+ null, null, null, 2, "live:health-promotion:edit", "养生科普编辑按钮"));
+ menuIds.add(saveMenu(6113L, 6110L, "删除养生科普", "live_health_promotion_delete", "B",
+ null, null, null, 3, "live:health-promotion:delete", "养生科普删除按钮"));
+ menuIds.add(saveMenu(6100L, 3000L, "宣传知识", "live_promotion_root", "M",
+ "/live/promotion", null, null, 20, null, "宣传知识目录"));
+ menuIds.add(saveMenu(6010L, 6100L, "项目介绍", "live_project_introduction_manage", "C",
+ "/live/promotion/project-introduction", "Live/ProjectIntroductionList", null, 60, "live:project-introduction:view", "项目介绍菜单"));
menuIds.add(saveMenu(6011L, 6010L, "新增项目介绍", "live_project_introduction_add", "B",
null, null, null, 1, "live:project-introduction:add", "项目介绍新增按钮"));
menuIds.add(saveMenu(6012L, 6010L, "编辑项目介绍", "live_project_introduction_edit", "B",
null, null, null, 2, "live:project-introduction:edit", "项目介绍编辑按钮"));
menuIds.add(saveMenu(6013L, 6010L, "删除项目介绍", "live_project_introduction_delete", "B",
null, null, null, 3, "live:project-introduction:delete", "项目介绍删除按钮"));
- menuIds.add(saveMenu(6014L, 3050L, "项目介绍密码", "live_project_introduction_password", "C",
- "/live/course/project-introduction-password", "Live/ProjectIntroductionPassword", null, 61, "live:project-introduction-password:view", "项目介绍密码菜单"));
+ menuIds.add(saveMenu(6014L, 6100L, "项目介绍密码", "live_project_introduction_password", "C",
+ "/live/promotion/project-introduction-password", "Live/ProjectIntroductionPassword", null, 61, "live:project-introduction-password:view", "项目介绍密码菜单"));
menuIds.add(saveMenu(6015L, 6014L, "编辑项目介绍密码", "live_project_introduction_password_edit", "B",
null, null, null, 1, "live:project-introduction-password:edit", "项目介绍密码编辑按钮"));
menuIds.add(saveMenu(4000L, 0L, "交易与财务", "trade_root", "M",
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthPopularizationSaveRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthPopularizationSaveRequest.java
index 9037336..9e069f8 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthPopularizationSaveRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/dto/HealthPopularizationSaveRequest.java
@@ -34,6 +34,9 @@
@Size(max = 255, message = "视频文件名长度不能超过255个字符")
private String videoFileName;
+ @Size(max = 255, message = "视频介绍长度不能超过255个字符")
+ private String videoDescription;
+
@NotNull(message = "请选择是否启用")
private Integer status;
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthPopularization.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthPopularization.java
index a14d43d..6e8559c 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthPopularization.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthPopularization.java
@@ -40,6 +40,9 @@
@TableField("video_file_name")
private String videoFileName;
+ @TableField("video_description")
+ private String videoDescription;
+
private Integer status;
@TableField("is_home_recommend")
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthPopularizationAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthPopularizationAppService.java
index f77cd77..3ea3a5f 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthPopularizationAppService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/HealthPopularizationAppService.java
@@ -96,6 +96,7 @@
item.setContent(content);
item.setVideoUrl(null);
item.setVideoFileName(null);
+ item.setVideoDescription(null);
return;
}
@@ -106,6 +107,7 @@
item.setContent(null);
item.setVideoUrl(videoUrl);
item.setVideoFileName(trimToNull(req.getVideoFileName()));
+ item.setVideoDescription(trimToNull(req.getVideoDescription()));
}
private int validateContentType(Integer contentType) {
@@ -153,6 +155,7 @@
vo.setContent(item.getContent());
vo.setVideoUrl(item.getVideoUrl());
vo.setVideoFileName(item.getVideoFileName());
+ vo.setVideoDescription(item.getVideoDescription());
vo.setStatus(item.getStatus());
vo.setIsHomeRecommend(item.getIsHomeRecommend());
vo.setClickCount(defaultNumber(item.getClickCount()));
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthPopularizationDetailVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthPopularizationDetailVO.java
index 40a4efa..e2ddc87 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthPopularizationDetailVO.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/HealthPopularizationDetailVO.java
@@ -17,6 +17,7 @@
private String content;
private String videoUrl;
private String videoFileName;
+ private String videoDescription;
private Integer status;
private Integer isHomeRecommend;
private Integer clickCount;
diff --git a/tcm-training-admin/config/routes.ts b/tcm-training-admin/config/routes.ts
index 201f113..484ca6e 100644
--- a/tcm-training-admin/config/routes.ts
+++ b/tcm-training-admin/config/routes.ts
@@ -343,52 +343,6 @@
component: './live/health',
},
{
- name: '健康科普',
- path: '/live/course/health-popularization',
- access: 'live:health-popularization:view',
- component: './live/health-popularization',
- },
- {
- name: '新增健康科普',
- path: '/live/course/health-popularization/create',
- hideInMenu: true,
- access: 'live:health-popularization:add',
- component: './live/health-popularization/manage',
- },
- {
- name: '编辑健康科普',
- path: '/live/course/health-popularization/:id/edit',
- hideInMenu: true,
- access: 'live:health-popularization:edit',
- component: './live/health-popularization/manage',
- },
- {
- name: '项目介绍',
- path: '/live/course/project-introduction',
- access: 'live:project-introduction:view',
- component: './live/project-introduction',
- },
- {
- name: '新增项目介绍',
- path: '/live/course/project-introduction/create',
- hideInMenu: true,
- access: 'live:project-introduction:add',
- component: './live/project-introduction/manage',
- },
- {
- name: '编辑项目介绍',
- path: '/live/course/project-introduction/:id/edit',
- hideInMenu: true,
- access: 'live:project-introduction:edit',
- component: './live/project-introduction/manage',
- },
- {
- name: '项目介绍密码',
- path: '/live/course/project-introduction-password',
- access: 'live:project-introduction:view',
- component: './live/project-introduction-password',
- },
- {
name: '视频与课程资源管理',
path: '/live/course/video-resources',
access: 'live:health-course:view',
@@ -463,6 +417,107 @@
access: 'live:expert:view',
component: './live/expert',
},
+ {
+ path: '/live/course/project-introduction/create',
+ hideInMenu: true,
+ redirect: '/live/promotion/project-introduction/create',
+ },
+ {
+ path: '/live/course/project-introduction',
+ hideInMenu: true,
+ redirect: '/live/promotion/project-introduction',
+ },
+ {
+ path: '/live/course/project-introduction-password',
+ hideInMenu: true,
+ redirect: '/live/promotion/project-introduction-password',
+ },
+ {
+ path: '/live/course/health-popularization/create',
+ hideInMenu: true,
+ redirect: '/live/promotion/health-popularization/create',
+ },
+ {
+ path: '/live/course/health-popularization',
+ hideInMenu: true,
+ redirect: '/live/promotion/health-popularization',
+ },
+ ],
+ },
+ {
+ path: '/live/promotion',
+ name: '宣传知识',
+ routes: [
+ {
+ path: '/live/promotion',
+ redirect: '/live/promotion/project-introduction',
+ },
+ {
+ name: '健康知识',
+ path: '/live/promotion/health-popularization',
+ access: 'live:health-popularization:view',
+ component: './live/health-popularization',
+ },
+ {
+ name: '新增健康知识',
+ path: '/live/promotion/health-popularization/create',
+ hideInMenu: true,
+ access: 'live:health-popularization:add',
+ component: './live/health-popularization/manage',
+ },
+ {
+ name: '编辑健康知识',
+ path: '/live/promotion/health-popularization/:id/edit',
+ hideInMenu: true,
+ access: 'live:health-popularization:edit',
+ component: './live/health-popularization/manage',
+ },
+ {
+ name: '养生科普',
+ path: '/live/promotion/health-promotion',
+ access: 'live:health-promotion:view',
+ component: './live/health-promotion',
+ },
+ {
+ name: '新增养生科普',
+ path: '/live/promotion/health-promotion/create',
+ hideInMenu: true,
+ access: 'live:health-promotion:add',
+ component: './live/health-promotion/manage',
+ },
+ {
+ name: '编辑养生科普',
+ path: '/live/promotion/health-promotion/:id/edit',
+ hideInMenu: true,
+ access: 'live:health-promotion:edit',
+ component: './live/health-promotion/manage',
+ },
+ {
+ name: '项目介绍',
+ path: '/live/promotion/project-introduction',
+ access: 'live:project-introduction:view',
+ component: './live/project-introduction',
+ },
+ {
+ name: '新增项目介绍',
+ path: '/live/promotion/project-introduction/create',
+ hideInMenu: true,
+ access: 'live:project-introduction:add',
+ component: './live/project-introduction/manage',
+ },
+ {
+ name: '编辑项目介绍',
+ path: '/live/promotion/project-introduction/:id/edit',
+ hideInMenu: true,
+ access: 'live:project-introduction:edit',
+ component: './live/project-introduction/manage',
+ },
+ {
+ name: '项目介绍密码',
+ path: '/live/promotion/project-introduction-password',
+ access: 'live:project-introduction:view',
+ component: './live/project-introduction-password',
+ },
],
},
],
diff --git a/tcm-training-admin/src/access.js b/tcm-training-admin/src/access.js
index be9aa14..8fcb466 100644
--- a/tcm-training-admin/src/access.js
+++ b/tcm-training-admin/src/access.js
@@ -39,7 +39,11 @@
!!permission?.['live:category:view'] ||
!!permission?.['live:reserve:view'] ||
!!permission?.['live:watch:view'] ||
- !!permission?.['live:expert:view'],
+ !!permission?.['live:expert:view'] ||
+ !!permission?.['live:project-introduction:view'] ||
+ !!permission?.['live:project-introduction-password:view'] ||
+ !!permission?.['live:health-popularization:view'] ||
+ !!permission?.['live:health-promotion:view'],
canViewTradeFinanceModule:
!!permission?.['course:purchase-order:view'] ||
!!permission?.['member:purchase-order:view'] ||
diff --git a/tcm-training-admin/src/pages/live/health-popularization/index.tsx b/tcm-training-admin/src/pages/live/health-popularization/index.tsx
index 2b45dc5..73543a2 100644
--- a/tcm-training-admin/src/pages/live/health-popularization/index.tsx
+++ b/tcm-training-admin/src/pages/live/health-popularization/index.tsx
@@ -87,7 +87,7 @@
const handleDelete = (record: PopularizationItem) => {
Modal.confirm({
- title: '确定要删除该健康科普吗?',
+ title: '确定要删除该健康知识吗?',
okText: '删除',
cancelText: '取消',
onOk: async () => {
@@ -159,7 +159,7 @@
{canEdit ? (
<button
type="button"
- onClick={() => history.push(`/live/course/health-popularization/${record.id}/edit`)}
+ onClick={() => history.push(`/live/promotion/health-popularization/${record.id}/edit`)}
>
编辑
</button>
@@ -177,7 +177,7 @@
return (
<div className="health-popularization-page">
<div className="health-popularization-page__panel">
- <div className="health-popularization-page__title">健康科普</div>
+ <div className="health-popularization-page__title">健康知识</div>
<div className="health-popularization-search-card">
<Form form={searchForm} layout="horizontal" className="health-popularization-search-form">
<Form.Item label="标题" name="keyword">
@@ -204,7 +204,7 @@
<div className="health-popularization-toolbar">
{canAdd ? (
- <Button type="primary" onClick={() => history.push('/live/course/health-popularization/create')}>
+ <Button type="primary" onClick={() => history.push('/live/promotion/health-popularization/create')}>
添加
</Button>
) : null}
diff --git a/tcm-training-admin/src/pages/live/health-popularization/manage.tsx b/tcm-training-admin/src/pages/live/health-popularization/manage.tsx
index d176f15..25cfcf3 100644
--- a/tcm-training-admin/src/pages/live/health-popularization/manage.tsx
+++ b/tcm-training-admin/src/pages/live/health-popularization/manage.tsx
@@ -20,7 +20,7 @@
const [coverImg, setCoverImg] = useState('');
const pathname = history.location.pathname;
const isCreate = pathname.endsWith('/create');
- const listPagePath = '/live/course/health-popularization';
+ const listPagePath = '/live/promotion/health-popularization';
const recordId = useMemo(() => {
if (isCreate) {
return '';
@@ -96,7 +96,7 @@
return (
<div className="health-popularization-page">
<div className="health-popularization-page__panel">
- <div className="health-popularization-page__title">{isCreate ? '添加健康科普' : '编辑健康科普'}</div>
+ <div className="health-popularization-page__title">{isCreate ? '添加健康知识' : '编辑健康知识'}</div>
<Form form={form} layout="vertical" className="health-popularization-edit-form">
<div className="health-popularization-edit-form__section-title">信息</div>
<div className="health-popularization-edit-form__grid">
diff --git a/tcm-training-admin/src/pages/live/health-promotion/index.tsx b/tcm-training-admin/src/pages/live/health-promotion/index.tsx
new file mode 100644
index 0000000..a1cec60
--- /dev/null
+++ b/tcm-training-admin/src/pages/live/health-promotion/index.tsx
@@ -0,0 +1,240 @@
+import {
+ deleteHealthPromotion,
+ getHealthPromotionPage,
+} from './service';
+import { history, useAccess } from '@umijs/max';
+import { Button, Form, Image, Input, Modal, Pagination, Select, Space, Table, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import React, { useEffect, useState } from 'react';
+import './style.less';
+
+type SearchValues = {
+ keyword?: string;
+ status?: number;
+};
+
+type PromotionItem = {
+ id: number;
+ title: string;
+ coverImg?: string;
+ clickCount?: number;
+ sort?: number;
+ contentType: number;
+ status: number;
+ createdAt: string;
+};
+
+const contentTypeOptions = [
+ { label: '图文', value: 1 },
+ { label: '视频', value: 2 },
+];
+
+const statusOptions = [
+ { label: '启用', value: 1 },
+ { label: '禁用', value: 0 },
+];
+
+// 养生科普只有视频内容,固定 contentType=2
+const trimSearchValues = (values: SearchValues) => ({
+ keyword: values.keyword?.trim() || undefined,
+ contentType: 2,
+ status: values.status,
+});
+
+const contentTypeText = (value?: number) =>
+ contentTypeOptions.find((item) => item.value === value)?.label || '-';
+
+const statusText = (value?: number) => (value === 1 ? '启用' : '禁用');
+
+const HealthPromotionPage: React.FC = () => {
+ const access = useAccess();
+ const [searchForm] = Form.useForm<SearchValues>();
+ const [list, setList] = useState<PromotionItem[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
+
+ const canView = access['live:health-promotion:view'];
+ const canAdd = access['live:health-promotion:add'];
+ const canEdit = access['live:health-promotion:edit'];
+ const canDelete = access['live:health-promotion:delete'];
+ const refreshKey = history.location.search;
+
+ const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => {
+ setLoading(true);
+ try {
+ const res = await getHealthPromotionPage({
+ page,
+ pageSize,
+ ...trimSearchValues(searchForm.getFieldsValue()),
+ });
+ setList(res.data.list || []);
+ setPagination({
+ page: Number(res.data.page || page),
+ pageSize: Number(res.data.pageSize || pageSize),
+ total: Number(res.data.total || 0),
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!canView) {
+ history.replace('/Welcome');
+ return;
+ }
+ loadList(1, 10);
+ }, [canView, refreshKey]);
+
+ const handleDelete = (record: PromotionItem) => {
+ Modal.confirm({
+ title: '确定要删除该养生科普吗?',
+ okText: '删除',
+ cancelText: '取消',
+ onOk: async () => {
+ await deleteHealthPromotion(record.id);
+ message.success('删除成功');
+ const nextPage = list.length === 1 && pagination.page > 1 ? pagination.page - 1 : pagination.page;
+ await loadList(nextPage, pagination.pageSize);
+ },
+ });
+ };
+
+ const columns: ColumnsType<PromotionItem> = [
+ {
+ title: '序号',
+ width: 72,
+ render: (_, __, index) => (pagination.page - 1) * pagination.pageSize + index + 1,
+ },
+ {
+ title: '标题',
+ dataIndex: 'title',
+ width: 320,
+ render: (_, record) => (
+ <div className="health-promotion-table__title">
+ {record.coverImg ? (
+ <Image src={record.coverImg} width={64} height={40} preview={false} />
+ ) : (
+ <div className="health-promotion-table__cover-placeholder">
+ {record.title?.slice(0, 2) || '-'}
+ </div>
+ )}
+ <span>{record.title || '-'}</span>
+ </div>
+ ),
+ },
+ {
+ title: '点击量',
+ dataIndex: 'clickCount',
+ width: 120,
+ render: (value) => value || 0,
+ },
+ {
+ title: '排序',
+ dataIndex: 'sort',
+ width: 100,
+ render: (value) => value ?? 0,
+ },
+ {
+ title: '类型',
+ dataIndex: 'contentType',
+ width: 100,
+ render: (value) => contentTypeText(value),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 100,
+ render: (value) => statusText(value),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createdAt',
+ width: 180,
+ },
+ {
+ title: '操作',
+ width: 180,
+ render: (_, record) => (
+ <Space size={16} className="health-promotion-table__actions">
+ {canEdit ? (
+ <button
+ type="button"
+ onClick={() => history.push(`/live/promotion/health-promotion/${record.id}/edit`)}
+ >
+ 编辑
+ </button>
+ ) : null}
+ {canDelete ? (
+ <button type="button" onClick={() => handleDelete(record)}>
+ 删除
+ </button>
+ ) : null}
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div className="health-promotion-page">
+ <div className="health-promotion-page__panel">
+ <div className="health-promotion-page__title">养生科普</div>
+ <div className="health-promotion-search-card">
+ <Form form={searchForm} layout="horizontal" className="health-promotion-search-form">
+ <Form.Item label="标题" name="keyword">
+ <Input placeholder="请输入" allowClear />
+ </Form.Item>
+ <Form.Item label="状态" name="status">
+ <Select placeholder="全部" allowClear options={statusOptions} />
+ </Form.Item>
+ <div className="health-promotion-search-form__actions">
+ <Button
+ onClick={async () => {
+ searchForm.resetFields();
+ await loadList(1, pagination.pageSize);
+ }}
+ >
+ 重置
+ </Button>
+ <Button type="primary" onClick={() => loadList(1, pagination.pageSize)}>
+ 搜索
+ </Button>
+ </div>
+ </Form>
+ </div>
+
+ <div className="health-promotion-toolbar">
+ {canAdd ? (
+ <Button type="primary" onClick={() => history.push('/live/promotion/health-promotion/create')}>
+ 添加
+ </Button>
+ ) : null}
+ </div>
+
+ <div className="health-promotion-table-card">
+ <Table<PromotionItem>
+ rowKey="id"
+ columns={columns}
+ dataSource={list}
+ loading={loading}
+ pagination={false}
+ scroll={{ x: 980 }}
+ />
+ <div className="health-promotion-pagination">
+ <Pagination
+ current={pagination.page}
+ pageSize={pagination.pageSize}
+ total={pagination.total}
+ showSizeChanger
+ showQuickJumper
+ showTotal={(total) => `共 ${total} 条`}
+ onChange={(page, pageSize) => loadList(page, pageSize)}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default HealthPromotionPage;
diff --git a/tcm-training-admin/src/pages/live/health-promotion/manage.tsx b/tcm-training-admin/src/pages/live/health-promotion/manage.tsx
new file mode 100644
index 0000000..86109d7
--- /dev/null
+++ b/tcm-training-admin/src/pages/live/health-promotion/manage.tsx
@@ -0,0 +1,240 @@
+import {
+ createHealthPromotion,
+ getHealthPromotionDetail,
+ updateHealthPromotion,
+ uploadHealthPromotionImage,
+ uploadHealthPromotionVideo,
+} from './service';
+import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
+import { history, useAccess } from '@umijs/max';
+import { Button, Form, Image, Input, InputNumber, Switch, Upload, message } from 'antd';
+import React, { useEffect, useMemo, useState } from 'react';
+import './style.less';
+
+const HealthPromotionManagePage: React.FC = () => {
+ const access = useAccess();
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [uploadingCover, setUploadingCover] = useState(false);
+ const [uploadingVideo, setUploadingVideo] = useState(false);
+ const [coverImg, setCoverImg] = useState('');
+ const pathname = history.location.pathname;
+ const isCreate = pathname.endsWith('/create');
+ const listPagePath = '/live/promotion/health-promotion';
+ const recordId = useMemo(() => {
+ if (isCreate) {
+ return '';
+ }
+ const segments = pathname.split('/');
+ return segments[segments.length - 2] || '';
+ }, [isCreate, pathname]);
+ const canAccess = isCreate ? access['live:health-promotion:add'] : access['live:health-promotion:edit'];
+
+ useEffect(() => {
+ if (!canAccess) {
+ history.replace('/Welcome');
+ return;
+ }
+ const bootstrap = async () => {
+ setLoading(true);
+ try {
+ if (isCreate) {
+ setCoverImg('');
+ form.setFieldsValue({
+ title: '',
+ videoUrl: '',
+ videoFileName: '',
+ videoDescription: '',
+ sort: 0,
+ status: true,
+ isHomeRecommend: true,
+ });
+ return;
+ }
+ const res = await getHealthPromotionDetail(recordId);
+ const detail = res.data;
+ setCoverImg(detail.coverImg || '');
+ form.setFieldsValue({
+ title: detail.title,
+ coverImg: detail.coverImg,
+ videoUrl: detail.videoUrl || '',
+ videoFileName: detail.videoFileName || '',
+ videoDescription: detail.videoDescription || '',
+ sort: detail.sort ?? 0,
+ status: detail.status === 1,
+ isHomeRecommend: detail.isHomeRecommend === 1,
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+ bootstrap();
+ }, [canAccess, form, isCreate, recordId]);
+
+ const handleSubmit = async () => {
+ const values = await form.validateFields();
+ const payload = {
+ title: values.title?.trim(),
+ coverImg,
+ // 养生科普固定为视频类型
+ contentType: 2,
+ videoUrl: values.videoUrl?.trim(),
+ videoFileName: values.videoFileName?.trim(),
+ videoDescription: values.videoDescription?.trim(),
+ sort: values.sort,
+ status: values.status ? 1 : 0,
+ isHomeRecommend: values.isHomeRecommend ? 1 : 0,
+ };
+ setSubmitting(true);
+ try {
+ if (isCreate) {
+ await createHealthPromotion(payload);
+ message.success('新增成功');
+ } else {
+ await updateHealthPromotion(recordId, payload);
+ message.success('保存成功');
+ }
+ history.replace(`${listPagePath}?refresh=${Date.now()}`);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="health-promotion-page">
+ <div className="health-promotion-page__panel">
+ <div className="health-promotion-page__title">{isCreate ? '添加养生科普' : '编辑养生科普'}</div>
+ <Form form={form} layout="vertical" className="health-promotion-edit-form">
+ <div className="health-promotion-edit-form__section-title">信息</div>
+ <div className="health-promotion-edit-form__grid">
+ <Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
+ <Input placeholder="请输入" maxLength={128} disabled={loading} />
+ </Form.Item>
+ <Form.Item label="视频封面图" className="health-promotion-edit-form__upload" required>
+ <Form.Item name="coverImg" rules={[{ required: true, message: '请上传视频封面图' }]} hidden>
+ <Input />
+ </Form.Item>
+ <Upload
+ accept="image/*"
+ showUploadList={false}
+ customRequest={async (options) => {
+ setUploadingCover(true);
+ try {
+ const res = await uploadHealthPromotionImage(options.file as File);
+ const url = res.data?.url;
+ if (!url) {
+ throw new Error('upload failed');
+ }
+ setCoverImg(url);
+ form.setFieldsValue({ coverImg: url });
+ options.onSuccess?.(res);
+ } catch (error) {
+ message.error('封面图上传失败');
+ options.onError?.(error as Error);
+ } finally {
+ setUploadingCover(false);
+ }
+ }}
+ >
+ <div className="health-promotion-edit-form__cover-box">
+ {coverImg ? (
+ <Image src={coverImg} width={96} height={96} preview={false} />
+ ) : uploadingCover ? (
+ '上传中...'
+ ) : (
+ <PlusOutlined />
+ )}
+ </div>
+ </Upload>
+ </Form.Item>
+ <div className="health-promotion-edit-form__switch-group">
+ <div className="health-promotion-edit-form__switch-item">
+ <span className="health-promotion-edit-form__switch-label">是否启用</span>
+ <Form.Item name="status" valuePropName="checked" rules={[{ required: true, message: '请选择是否启用' }]}>
+ <Switch />
+ </Form.Item>
+ </div>
+ <div className="health-promotion-edit-form__switch-item">
+ <span className="health-promotion-edit-form__switch-label">首页推荐</span>
+ <Form.Item
+ name="isHomeRecommend"
+ valuePropName="checked"
+ rules={[{ required: true, message: '请选择是否首页推荐' }]}
+ >
+ <Switch />
+ </Form.Item>
+ </div>
+ </div>
+ <Form.Item label="排序" name="sort" rules={[{ required: true, message: '请输入排序' }]}>
+ <InputNumber min={0} precision={0} placeholder="请输入" style={{ width: '100%' }} disabled={loading} />
+ </Form.Item>
+ <Form.Item
+ label="视频地址"
+ name="videoUrl"
+ extra="本地视频和视频地址必须填一个"
+ rules={[{ required: true, message: '请填写视频地址或上传本地视频' }]}
+ >
+ <Input placeholder="请输入" disabled={loading} />
+ </Form.Item>
+ <Form.Item
+ label="视频介绍"
+ name="videoDescription"
+ rules={[{ required: true, message: '请输入视频介绍' }]}
+ >
+ <Input.TextArea placeholder="请输入视频介绍" maxLength={255} rows={3} disabled={loading} />
+ </Form.Item>
+ <Form.Item label="本地视频" extra="本地视频和视频地址必须填一个">
+ <div className="health-promotion-edit-form__video-upload">
+ <Upload
+ accept="video/*"
+ showUploadList={false}
+ customRequest={async (options) => {
+ setUploadingVideo(true);
+ try {
+ const res = await uploadHealthPromotionVideo(options.file as File);
+ const url = res.data?.url;
+ if (!url) {
+ throw new Error('upload failed');
+ }
+ form.setFieldsValue({
+ videoUrl: url,
+ videoFileName: res.data?.fileName || (options.file as File).name,
+ });
+ message.success('视频上传成功');
+ options.onSuccess?.(res);
+ } catch (error) {
+ message.error('视频上传失败');
+ options.onError?.(error as Error);
+ } finally {
+ setUploadingVideo(false);
+ }
+ }}
+ >
+ <Button icon={<UploadOutlined />} loading={uploadingVideo}>
+ 上传
+ </Button>
+ </Upload>
+ <span>
+ {form.getFieldValue('videoFileName') ||
+ (form.getFieldValue('videoUrl') ? '已填写视频地址' : '未上传视频文件')}
+ </span>
+ </div>
+ <Form.Item name="videoFileName" hidden>
+ <Input />
+ </Form.Item>
+ </Form.Item>
+ </div>
+ <div className="health-promotion-edit-form__footer">
+ <Button onClick={() => history.push(listPagePath)}>取消</Button>
+ <Button type="primary" loading={submitting} onClick={handleSubmit}>
+ 保存
+ </Button>
+ </div>
+ </Form>
+ </div>
+ </div>
+ );
+};
+
+export default HealthPromotionManagePage;
diff --git a/tcm-training-admin/src/pages/live/health-promotion/service.ts b/tcm-training-admin/src/pages/live/health-promotion/service.ts
new file mode 100644
index 0000000..60a62fd
--- /dev/null
+++ b/tcm-training-admin/src/pages/live/health-promotion/service.ts
@@ -0,0 +1,55 @@
+import { request } from '@umijs/max';
+
+// 养生科普与健康知识共用后端接口(同一张表 tcm_health_popularization),
+// 通过 contentType=2(视频)区分养生科普,contentType=1(图文)区分健康知识。
+
+export async function getHealthPromotionPage(params?: Record<string, any>) {
+ return request('/health-popularizations', {
+ method: 'GET',
+ params,
+ });
+}
+
+export async function getHealthPromotionDetail(id: number | string) {
+ return request(`/health-popularizations/${id}`, {
+ method: 'GET',
+ });
+}
+
+export async function createHealthPromotion(data: Record<string, any>) {
+ return request('/health-popularizations', {
+ method: 'POST',
+ data,
+ });
+}
+
+export async function updateHealthPromotion(id: number | string, data: Record<string, any>) {
+ return request(`/health-popularizations/${id}`, {
+ method: 'PUT',
+ data,
+ });
+}
+
+export async function deleteHealthPromotion(id: number | string) {
+ return request(`/health-popularizations/${id}`, {
+ method: 'DELETE',
+ });
+}
+
+export async function uploadHealthPromotionImage(file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return request('/upload/image', {
+ method: 'POST',
+ data: formData,
+ });
+}
+
+export async function uploadHealthPromotionVideo(file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return request('/upload/video', {
+ method: 'POST',
+ data: formData,
+ });
+}
diff --git a/tcm-training-admin/src/pages/live/health-promotion/style.less b/tcm-training-admin/src/pages/live/health-promotion/style.less
new file mode 100644
index 0000000..af2eaca
--- /dev/null
+++ b/tcm-training-admin/src/pages/live/health-promotion/style.less
@@ -0,0 +1,223 @@
+.health-promotion-page {
+ min-height: calc(100vh - 48px);
+ padding: 18px 20px 24px;
+ background: #f2f5f9;
+}
+
+.health-promotion-page__panel {
+ padding: 18px 18px 24px;
+ border-radius: 10px;
+ background: #fff;
+ box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.health-promotion-page__title {
+ margin-bottom: 16px;
+ color: #303133;
+ font-size: 20px;
+ font-weight: 500;
+}
+
+.health-promotion-search-card {
+ padding: 16px 18px 10px;
+ border: 1px solid #e7edf4;
+ border-radius: 6px;
+}
+
+.health-promotion-search-form {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(200px, 1fr)) auto;
+ gap: 10px 18px;
+ align-items: start;
+}
+
+.health-promotion-search-form .ant-form-item {
+ margin-bottom: 0;
+}
+
+.health-promotion-search-form .ant-form-item-row {
+ display: grid;
+ grid-template-columns: 64px minmax(0, 1fr);
+ align-items: center;
+ column-gap: 12px;
+}
+
+.health-promotion-search-form .ant-form-item-label {
+ padding: 0;
+}
+
+.health-promotion-search-form__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ min-width: 160px;
+}
+
+.health-promotion-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin: 16px 0 10px;
+}
+
+.health-promotion-table-card {
+ overflow: hidden;
+ border: 1px solid #e7edf4;
+ border-radius: 6px;
+}
+
+.health-promotion-table-card .ant-table-thead > tr > th {
+ background: #f8fafc;
+ color: #303133;
+ font-weight: 500;
+}
+
+.health-promotion-table__title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.health-promotion-table__cover-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 64px;
+ height: 40px;
+ border-radius: 6px;
+ background: #edf2f7;
+ color: #64748b;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.health-promotion-table__actions {
+ button {
+ padding: 0;
+ border: none;
+ background: none;
+ color: #1677ff;
+ cursor: pointer;
+ }
+}
+
+.health-promotion-pagination {
+ display: flex;
+ justify-content: flex-end;
+ padding: 16px 18px;
+}
+
+.health-promotion-edit-form__section-title {
+ margin-bottom: 16px;
+ padding-left: 8px;
+ border-left: 3px solid #1677ff;
+ color: #303133;
+ font-size: 15px;
+ font-weight: 500;
+}
+
+.health-promotion-edit-form__grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0 36px;
+ align-items: start;
+}
+
+.health-promotion-edit-form .ant-form-item {
+ margin-bottom: 18px;
+}
+
+.health-promotion-edit-form__upload {
+ grid-column: span 1;
+}
+
+.health-promotion-edit-form__cover-box {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 96px;
+ height: 96px;
+ overflow: hidden;
+ border: 1px dashed #cbd5e1;
+ border-radius: 8px;
+ background: #f8fafc;
+ color: #94a3b8;
+ font-size: 28px;
+ cursor: pointer;
+}
+
+.health-promotion-edit-form__switch-group {
+ display: flex;
+ align-items: center;
+ gap: 28px;
+ min-height: 72px;
+}
+
+.health-promotion-edit-form__switch-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.health-promotion-edit-form__switch-item .ant-form-item {
+ margin-bottom: 0;
+}
+
+.health-promotion-edit-form__switch-label {
+ color: #606266;
+ white-space: nowrap;
+}
+
+.health-promotion-edit-form__video-upload {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.health-promotion-edit-form__video-upload span {
+ color: #606266;
+ word-break: break-all;
+}
+
+.health-promotion-edit-form__footer {
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+@media (max-width: 1200px) {
+ .health-promotion-search-form {
+ grid-template-columns: repeat(2, minmax(200px, 1fr));
+ }
+
+ .health-promotion-edit-form__grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 768px) {
+ .health-promotion-page {
+ padding: 12px;
+ }
+
+ .health-promotion-page__panel {
+ padding: 16px;
+ }
+
+ .health-promotion-search-form {
+ grid-template-columns: 1fr;
+ }
+
+ .health-promotion-search-form__actions {
+ justify-content: flex-start;
+ }
+
+ .health-promotion-edit-form__grid {
+ grid-template-columns: 1fr;
+ }
+
+ .health-promotion-edit-form__switch-group,
+ .health-promotion-edit-form__video-upload {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
diff --git a/tcm-training-admin/src/pages/live/project-introduction/index.tsx b/tcm-training-admin/src/pages/live/project-introduction/index.tsx
index ab4df30..c041dd5 100644
--- a/tcm-training-admin/src/pages/live/project-introduction/index.tsx
+++ b/tcm-training-admin/src/pages/live/project-introduction/index.tsx
@@ -177,7 +177,7 @@
{canEdit ? (
<button
type="button"
- onClick={() => history.push(`/live/course/project-introduction/${record.id}/edit`)}
+ onClick={() => history.push(`/live/promotion/project-introduction/${record.id}/edit`)}
>
编辑
</button>
@@ -228,7 +228,7 @@
<div className="project-introduction-toolbar">
{canAdd ? (
- <Button type="primary" onClick={() => history.push('/live/course/project-introduction/create')}>
+ <Button type="primary" onClick={() => history.push('/live/promotion/project-introduction/create')}>
添加
</Button>
) : null}
diff --git a/tcm-training-admin/src/pages/live/project-introduction/manage.tsx b/tcm-training-admin/src/pages/live/project-introduction/manage.tsx
index 0f6797b..9dffd59 100644
--- a/tcm-training-admin/src/pages/live/project-introduction/manage.tsx
+++ b/tcm-training-admin/src/pages/live/project-introduction/manage.tsx
@@ -33,7 +33,7 @@
const [coverImg, setCoverImg] = useState('');
const pathname = history.location.pathname;
const isCreate = pathname.endsWith('/create');
- const listPagePath = '/live/course/project-introduction';
+ const listPagePath = '/live/promotion/project-introduction';
const recordId = useMemo(() => {
if (isCreate) {
return '';
diff --git a/traditionalApp/manifest.json b/traditionalApp/manifest.json
index aa91d68..71fca00 100644
--- a/traditionalApp/manifest.json
+++ b/traditionalApp/manifest.json
@@ -1,9 +1,9 @@
{
"sassImplementationName" : "node-sass",
"name" : "海诊康道",
- "appid" : "__UNI__B2F54BE",
+ "appid" : "__UNI__101009D",
"description" : "",
- "versionName" : "1.0.22",
+ "versionName" : "1.0.23",
"versionCode" : 122,
"transformPx" : false,
/* 5+App特有相关 */
diff --git a/traditionalApp/pages.json b/traditionalApp/pages.json
index 8506151..0d9da6e 100644
--- a/traditionalApp/pages.json
+++ b/traditionalApp/pages.json
@@ -170,6 +170,12 @@
}
},
{
+ "path": "pages/weizanLive/weizanLive",
+ "style": {
+ "navigationBarTitleText": "微赞直播"
+ }
+ },
+ {
"path": "pages/feedback/feedback",
"style": {
"navigationStyle": "custom"
diff --git a/traditionalApp/pages/articleDetail/articleDetail.vue b/traditionalApp/pages/articleDetail/articleDetail.vue
index cbbb0f5..90597c9 100644
--- a/traditionalApp/pages/articleDetail/articleDetail.vue
+++ b/traditionalApp/pages/articleDetail/articleDetail.vue
@@ -26,6 +26,11 @@
<rich-text v-if="!isVideoType" :nodes="contentNodes" class="rich-content"></rich-text>
<view v-if="loading" class="status-text">加载中...</view>
<view v-else-if="!detail.id" class="status-text">暂无详情内容</view>
+
+ <!-- 视频介绍:仅视频类型且有介绍文案时展示 -->
+ <view v-if="isVideoType && videoDescription" class="video-intro">
+ <text class="video-intro-text">{{ videoDescription }}</text>
+ </view>
</view>
</view>
</template>
@@ -45,6 +50,7 @@
content : string
videoUrl : string
videoFileName : string
+ videoDescription : string
}
const statusBarHeight = ref(0)
@@ -66,6 +72,7 @@
content: '',
videoUrl: '',
videoFileName: '',
+ videoDescription: '',
}
}
@@ -83,12 +90,18 @@
})
/**
+ * 视频介绍文案,去除首尾空白后用于详情底部展示。
+ */
+ const videoDescription = computed(() => String(detail.value.videoDescription || '').trim())
+
+ /**
* 将富文本正文转换为 `rich-text` 可直接消费的节点字符串。
*/
const contentNodes = computed(() => buildRichTextNodes(detail.value.content, '暂无详情内容'))
/**
- * 初始化导航栏所需的状态栏高度。
+ * 初始化导航栏所需的状态栏高度;详情加载已在 onLoad 中完成,
+ * 此处仅在 onLoad 未取到主键时做一次兜底,避免详情接口被请求两次。
*/
onMounted(() => {
uni.getSystemInfo({
@@ -96,12 +109,15 @@
statusBarHeight.value = res.statusBarHeight || 0
}
})
+ if (popularizationId.value) {
+ return
+ }
setTimeout(() => {
const pageOptions = getCurrentPageOptions()
- if (!popularizationId.value) {
- popularizationId.value = normalizeRouteParam(pageOptions?.id)
+ popularizationId.value = normalizeRouteParam(pageOptions?.id)
+ if (popularizationId.value) {
+ loadDetail()
}
- loadDetail()
}, 0)
})
@@ -285,4 +301,19 @@
line-height: 40rpx;
color: #86909C;
}
+
+ .video-intro {
+ margin-top: 32rpx;
+ padding-top: 24rpx;
+ border-top: 1rpx solid #EBEDF0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .video-intro-text {
+ font-size: 28rpx;
+ line-height: 1.8;
+ color: #5B6475;
+ white-space: pre-wrap;
+ }
</style>
diff --git a/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue b/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
index 48956ac..7d12012 100644
--- a/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
+++ b/traditionalApp/pages/healthCoursesHub/healthCoursesHub.vue
@@ -39,20 +39,20 @@
}
/**
- * 进入复制后的养生科普二级页。
+ * 进入复制后的养生科普二级页(contentType=2 仅视频)。
*/
const goHealthPopularization = () => {
uni.navigateTo({
- url: '/pages/healthPopularization/healthPopularization'
+ url: '/pages/healthPopularization/healthPopularization?contentType=2'
})
}
/**
- * 进入原健康科普页,作为「健康知识」二级页。
+ * 进入原健康科普页,作为「健康知识」二级页(contentType=1 仅图文)。
*/
const goHealthKnowledge = () => {
uni.navigateTo({
- url: '/pages/publish/publish'
+ url: '/pages/publish/publish?contentType=1'
})
}
</script>
diff --git a/traditionalApp/pages/healthPopularization/healthPopularization.vue b/traditionalApp/pages/healthPopularization/healthPopularization.vue
index 140f81a..8a8045f 100644
--- a/traditionalApp/pages/healthPopularization/healthPopularization.vue
+++ b/traditionalApp/pages/healthPopularization/healthPopularization.vue
@@ -12,9 +12,7 @@
<view v-if="articleList.length" class="article-list">
<view class="article-item" v-for="item in articleList" :key="item.id" @tap="goToDetail(item.id)">
<image class="article-img" :src="item.coverImage || defaultCoverImage" mode="aspectFill"></image>
- <view class="article-shadow">
- <text class="article-text">{{ item.title }}</text>
- </view>
+ <text class="article-title">{{ item.title }}</text>
</view>
</view>
<view v-else-if="loading" class="status-text">加载中...</view>
@@ -25,7 +23,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
-import { onPullDownRefresh } from '@dcloudio/uni-app'
+import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getHealthPopularizationList } from './service'
interface PopularizationListItem {
@@ -40,6 +38,18 @@
const loading = ref(false)
const articleList = ref<PopularizationListItem[]>([])
const defaultCoverImage = '/static/image/home/zixun.png'
+// 内容类型:2 养生科普,默认按养生科普过滤。
+const contentType = ref<number>(2)
+
+/**
+ * 读取路由参数中的内容类型,用于按类型过滤列表。
+ */
+onLoad((options) => {
+ const raw = Number(options?.contentType)
+ if (!Number.isNaN(raw) && raw > 0) {
+ contentType.value = raw
+ }
+})
/**
* 页面加载时初始化状态栏高度并拉取养生科普列表。
@@ -84,7 +94,7 @@
async function loadPopularizationList() {
loading.value = true
try {
- const response = await getHealthPopularizationList()
+ const response = await getHealthPopularizationList(contentType.value)
if (response?.code !== 0 || !Array.isArray(response.data)) {
articleList.value = []
return
@@ -163,43 +173,36 @@
.article-list {
display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
+ flex-direction: column;
}
.article-item {
- width: 328rpx;
- height: 328rpx;
- border-radius: 16rpx;
- position: relative;
- overflow: hidden;
- margin-bottom: 24rpx;
-}
-
-.article-img {
- width: 100%;
- height: 100%;
-}
-
-.article-shadow {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 64rpx;
- background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
display: flex;
align-items: center;
- padding: 0 16rpx;
+ width: 100%;
+ padding: 24rpx 0;
+ border-bottom: 1rpx solid #F0F0F0;
box-sizing: border-box;
}
-.article-text {
- color: #FFFFFF;
- font-size: 24rpx;
- white-space: nowrap;
+.article-img {
+ width: 200rpx;
+ height: 150rpx;
+ border-radius: 12rpx;
+ flex-shrink: 0;
+ background-color: #F5F6F8;
+}
+
+.article-title {
+ flex: 1;
+ margin-left: 24rpx;
+ font-size: 30rpx;
+ line-height: 44rpx;
+ color: #333;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
- width: 100%;
}
</style>
diff --git a/traditionalApp/pages/healthPopularization/service.js b/traditionalApp/pages/healthPopularization/service.js
index 419cab1..66cc99b 100644
--- a/traditionalApp/pages/healthPopularization/service.js
+++ b/traditionalApp/pages/healthPopularization/service.js
@@ -2,7 +2,9 @@
/**
* 获取 APP 养生科普列表(与原健康科普共用接口)。
+ * @param {number} [contentType] 内容类型:1 健康知识、2 养生科普;不传返回全部。
*/
-export const getHealthPopularizationList = () => {
- return request.get('/api/app/health-popularizations', {}, false, false)
+export const getHealthPopularizationList = (contentType) => {
+ const params = contentType == null ? {} : { contentType }
+ return request.get('/api/app/health-popularizations', params, false, false)
}
diff --git a/traditionalApp/pages/home/home.vue b/traditionalApp/pages/home/home.vue
index 0cc782e..8d4761b 100644
--- a/traditionalApp/pages/home/home.vue
+++ b/traditionalApp/pages/home/home.vue
@@ -379,7 +379,7 @@
return;
}
- const webviewUrl = `/pages/webview/webview?url=${encodeURIComponent(targetUrl)}&title=${encodeURIComponent('微赞直播')}&clearCache=1`;
+ const webviewUrl = `/pages/weizanLive/weizanLive?url=${encodeURIComponent(targetUrl)}&title=${encodeURIComponent('微赞直播')}`;
console.log('[goWeiZanLive] 准备跳转 WebView:', webviewUrl);
uni.navigateTo({
diff --git a/traditionalApp/pages/publish/publish.vue b/traditionalApp/pages/publish/publish.vue
index 9f48c75..e13aa36 100644
--- a/traditionalApp/pages/publish/publish.vue
+++ b/traditionalApp/pages/publish/publish.vue
@@ -15,9 +15,7 @@
<view v-if="articleList.length" class="article-list">
<view class="article-item" v-for="item in articleList" :key="item.id" @tap="goToDetail(item.id)">
<image class="article-img" :src="item.coverImage || defaultCoverImage" mode="aspectFill"></image>
- <view class="article-shadow">
- <text class="article-text">{{ item.title }}</text>
- </view>
+ <text class="article-title">{{ item.title }}</text>
</view>
</view>
<view v-else-if="loading" class="status-text">加载中...</view>
@@ -28,7 +26,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
-import { onPullDownRefresh } from '@dcloudio/uni-app'
+import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import { getHealthPopularizationList } from './service'
interface PopularizationListItem {
@@ -43,6 +41,18 @@
const loading = ref(false)
const articleList = ref<PopularizationListItem[]>([])
const defaultCoverImage = '/static/image/home/zixun.png'
+// 内容类型:1 健康知识,默认按健康知识过滤。
+const contentType = ref<number>(1)
+
+/**
+ * 读取路由参数中的内容类型,用于按类型过滤列表。
+ */
+onLoad((options) => {
+ const raw = Number(options?.contentType)
+ if (!Number.isNaN(raw) && raw > 0) {
+ contentType.value = raw
+ }
+})
/**
* 页面加载时初始化状态栏高度并拉取健康知识列表。
@@ -87,7 +97,7 @@
async function loadPopularizationList() {
loading.value = true
try {
- const response = await getHealthPopularizationList()
+ const response = await getHealthPopularizationList(contentType.value)
if (response?.code !== 0 || !Array.isArray(response.data)) {
articleList.value = []
return
@@ -166,43 +176,36 @@
.article-list {
display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
+ flex-direction: column;
}
.article-item {
- width: 328rpx;
- height: 328rpx;
- border-radius: 16rpx;
- position: relative;
- overflow: hidden;
- margin-bottom: 24rpx;
-}
-
-.article-img {
- width: 100%;
- height: 100%;
-}
-
-.article-shadow {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 64rpx;
- background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%);
display: flex;
align-items: center;
- padding: 0 16rpx;
+ width: 100%;
+ padding: 24rpx 0;
+ border-bottom: 1rpx solid #F0F0F0;
box-sizing: border-box;
}
-.article-text {
- color: #FFFFFF;
- font-size: 24rpx;
- white-space: nowrap;
+.article-img {
+ width: 200rpx;
+ height: 150rpx;
+ border-radius: 12rpx;
+ flex-shrink: 0;
+ background-color: #F5F6F8;
+}
+
+.article-title {
+ flex: 1;
+ margin-left: 24rpx;
+ font-size: 30rpx;
+ line-height: 44rpx;
+ color: #333;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
- width: 100%;
}
</style>
diff --git a/traditionalApp/pages/publish/service.js b/traditionalApp/pages/publish/service.js
index a42ca25..914e865 100644
--- a/traditionalApp/pages/publish/service.js
+++ b/traditionalApp/pages/publish/service.js
@@ -2,7 +2,9 @@
/**
* 获取 APP 健康科普列表。
+ * @param {number} [contentType] 内容类型:1 健康知识、2 养生科普;不传返回全部。
*/
-export const getHealthPopularizationList = () => {
- return request.get('/api/app/health-popularizations', {}, false, false)
+export const getHealthPopularizationList = (contentType) => {
+ const params = contentType == null ? {} : { contentType }
+ return request.get('/api/app/health-popularizations', params, false, false)
}
diff --git a/traditionalApp/pages/weizanLive/weizanLive.vue b/traditionalApp/pages/weizanLive/weizanLive.vue
new file mode 100644
index 0000000..d9f0f57
--- /dev/null
+++ b/traditionalApp/pages/weizanLive/weizanLive.vue
@@ -0,0 +1,61 @@
+<template>
+ <view class="container">
+ <web-view v-if="webViewUrl" class="webview-content" :src="webViewUrl"></web-view>
+ <view v-else class="empty-box"><text class="empty-text">链接地址无效</text></view>
+ </view>
+</template>
+<script setup>
+import { ref } from 'vue'
+import { onLoad, onUnload } from '@dcloudio/uni-app'
+import { clearWebViewData } from '@/utils/webview-cache.js'
+
+const webViewUrl = ref('')
+const WEIXIN_APP_ID = 'wx074c1f0cf5a0ecf4'
+let originalUserAgent = ''
+
+onLoad((options) => {
+ const url = decodeValue(options?.url)
+ const title = decodeValue(options?.title)
+ if (title) uni.setNavigationBarTitle({ title })
+ // #ifdef APP-PLUS
+ clearWebViewData()
+ injectUserAgent()
+ console.log('[weizanLive] 跳转前完整 UA:', plus.navigator.getUserAgent() || '')
+ // #endif
+ webViewUrl.value = url
+})
+
+onUnload(() => {
+ // #ifdef APP-PLUS
+ if (originalUserAgent) {
+ try { plus.navigator.setUserAgent(originalUserAgent, false) } catch (e) {}
+ originalUserAgent = ''
+ }
+ // #endif
+})
+
+function injectUserAgent() {
+ // #ifdef APP-PLUS
+ try {
+ const baseUA = plus.navigator.getUserAgent() || ''
+ if (baseUA.indexOf('webchatPay/') >= 0) {
+ originalUserAgent = baseUA.replace(/\s*webchatPay\/\S+/g, '')
+ return
+ }
+ originalUserAgent = baseUA
+ plus.navigator.setUserAgent(`${baseUA} webchatPay/${WEIXIN_APP_ID}`.trim(), false)
+ } catch (e) { console.error('[weizanLive] UA 注入失败:', e) }
+ // #endif
+}
+
+function decodeValue(value) {
+ if (typeof value !== 'string' || !value.trim()) return ''
+ try { return decodeURIComponent(value) } catch (e) { return value.trim() }
+}
+</script>
+<style lang="scss" scoped>
+.container { width: 100vw; min-height: 100vh; background: #fff; }
+.webview-content { width: 100%; height: 100vh; }
+.empty-box { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
+.empty-text { font-size: 28rpx; color: #999; }
+</style>
diff --git a/traditionalApp/utils/weizan-pay-bridge.js b/traditionalApp/utils/weizan-pay-bridge.js
index 677c954..e910e86 100644
--- a/traditionalApp/utils/weizan-pay-bridge.js
+++ b/traditionalApp/utils/weizan-pay-bridge.js
@@ -17,6 +17,7 @@
const PAY_BRIDGE_NAME = 'jumpToMiniProgramPay'
const PAY_BRIDGE_ANDROID_KEY = 'android' // H5 调 window.android.xxx
+const LOG_TAG = '[weizanPay]'
/**
* 给指定 webview 注册微赞支付 JSBridge。
@@ -52,9 +53,12 @@
try {
const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase()
if (platform === 'android') {
- // Android 移除注入的 JS 接口
+ // Android 移除注入的 JS 接口(同样要作用在原生 WebView 上)
try {
- plus.android.invoke(webviewInstance, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY)
+ const nativeWebView = getAndroidNativeWebView(webviewInstance)
+ if (nativeWebView) {
+ plus.android.invoke(nativeWebView, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY)
+ }
} catch (e) {}
}
// iOS:WKUserContentController 的 messageHandler 通常随 webview 销毁自动回收,
@@ -115,15 +119,33 @@
// ===== Android 实现:注入 window.android.jumpToMiniProgramPay =====
function registerAndroidPayBridge(webviewInstance, onPay) {
+ console.log(LOG_TAG, '[Android] 开始注册 JSBridge,注入 key=', PAY_BRIDGE_ANDROID_KEY, '方法名=', PAY_BRIDGE_NAME)
+ console.log(LOG_TAG, '[Android] webviewInstance=', webviewInstance ? '[Object]' : 'null')
+ // plus.webview.create 返回的是 JS 包装对象,不能直接反射;
+ // 必须先取底层原生 WebView(5+ ADWebView,是 android.webkit.WebView 的子类)再反射,
+ // 否则 getSettings / addJavascriptInterface 都会失败。
+ const nativeWebView = getAndroidNativeWebView(webviewInstance)
+ console.log(LOG_TAG, '[Android] nativeInstanceObject=', nativeWebView ? '[Object]' : 'null')
+ // 反射验证是否是真原生 WebView(应暴露 getSettings)
+ const settings = safeAndroidInvoke(nativeWebView, 'getSettings')
+ console.log(LOG_TAG, '[Android] getSettings 反射结果=', settings ? '[Object]' : 'null')
+ if (!settings) {
+ console.error(LOG_TAG, '[Android] 未取到原生 WebView 或不暴露 getSettings,注册中止')
+ console.error(LOG_TAG, '[Android] 请确认调用方传入的是 plus.webview.create 创建的子 webview,而不是 currentWebview()')
+ return
+ }
try {
- const JsInterface = plus.android.implements('io.dcloud.js.WeizanPayJsInterface', {
+ // java.io.Serializable 一定存在、无强制方法,足以承载动态代理
+ // H5 调 window.android.xxx 时由 Proxy 拦截转发到 JS 函数
+ const JsInterface = plus.android.implements('java.io.Serializable', {
[PAY_BRIDGE_NAME]: function (paramsJson) {
- // 注意:此日志运行在原生子线程
- console.log('[weizanPay][Android] JS 接口被触发, 收到原始字符串:', paramsJson)
+ console.log(LOG_TAG, '===== [Android] H5 调用 window.android.' + PAY_BRIDGE_NAME + ' =====')
+ console.log(LOG_TAG, '[Android] 原始 paramsJson=', safeStringify(paramsJson))
// JS 接口回调在原生子线程,必须切回 UI 线程才能拉起微信 SDK
plus.android.runOnUiThread(() => {
- console.log('[weizanPay][Android] 已切回 UI 线程, 开始解析参数')
+ console.log(LOG_TAG, '[Android] 已进入 UI 线程,开始解析参数')
const params = normalizePayParams(parseJsonSafe(paramsJson))
+ console.log(LOG_TAG, '[Android] 解析后 params=', safeStringify(params))
if (typeof onPay === 'function') {
try { onPay(params) } catch (e) {}
}
@@ -131,64 +153,189 @@
})
}
})
- plus.android.invoke(webviewInstance, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY)
- console.log('[weizanPay][Android] JSBridge 已注册')
+ console.log(LOG_TAG, '[Android] JsInterface 已创建:', JsInterface ? '[Object]' : 'null')
+ // 注入到原生 WebView(而非 JS 包装对象),H5 端才能拿到 window.android.xxx
+ plus.android.invoke(nativeWebView, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY)
+ console.log(LOG_TAG, '[Android] addJavascriptInterface 完成,window.' + PAY_BRIDGE_ANDROID_KEY + ' 已注入')
+ // 通过 evalJS 验证 H5 端 window.android 是否真的能访问到
+ try {
+ if (typeof webviewInstance.evalJS === 'function') {
+ webviewInstance.evalJS(`
+ try {
+ if (typeof window.android === 'undefined' || window.android === null) {
+ console.log('[weizanPay][H5] window.android 不存在');
+ } else {
+ console.log('[weizanPay][H5] window.android 存在, keys=' + Object.keys(window.android).join(','));
+ console.log('[weizanPay][H5] window.android.${PAY_BRIDGE_NAME} 类型=' + typeof window.android.${PAY_BRIDGE_NAME});
+ }
+ } catch (e) {
+ console.log('[weizanPay][H5] 检查 window.android 异常: ' + e.message);
+ }
+ `)
+ }
+ } catch (e) {
+ console.warn(LOG_TAG, '[Android] evalJS 验证失败:', e && e.message)
+ }
} catch (e) {
- console.error('[weizanPay][Android] 注册失败:', e)
+ console.error(LOG_TAG, '[Android] 注册 JSBridge 失败:', e)
+ console.error(LOG_TAG, '[Android] 异常 message=', e && e.message)
+ }
+}
+
+/**
+ * 取 plus.webview 实例底层的原生 android.webkit.WebView。
+ * plus.webview.create 返回的是 JS 包装对象,直接反射 getSettings/addJavascriptInterface 会失败,
+ * 必须先取到原生 WebView。优先用官方 nativeInstanceObject(),失败再退回 obtainWebview()。
+ */
+function getAndroidNativeWebView(webview) {
+ if (!webview) {
+ return null
+ }
+ // 1. 官方 API:WebviewObject.nativeInstanceObject() 返回原生 WebView
+ if (typeof webview.nativeInstanceObject === 'function') {
+ try {
+ const nativeObj = webview.nativeInstanceObject()
+ if (nativeObj) {
+ return nativeObj
+ }
+ } catch (e) {
+ console.warn(LOG_TAG, '[Android] nativeInstanceObject 调用失败:', e && e.message)
+ }
+ }
+ // 2. 兼容:部分运行时通过 obtainWebview 取原生 WebView
+ if (typeof webview.obtainWebview === 'function') {
+ try {
+ const obtained = webview.obtainWebview()
+ if (obtained) {
+ return obtained
+ }
+ } catch (e) {}
+ }
+ return null
+}
+
+/**
+ * 安全调用 plus.android.invoke,失败返回 null。
+ */
+function safeAndroidInvoke(obj, methodName, ...args) {
+ try {
+ return plus.android.invoke(obj, methodName, ...args)
+ } catch (e) {
+ return null
}
}
// ===== iOS 实现:注册 window.webkit.messageHandlers.jumpToMiniProgramPay =====
function registerIOSPayBridge(webviewInstance, onPay) {
+ console.log(LOG_TAG, '[iOS] 开始注册 JSBridge,handler name=', PAY_BRIDGE_NAME)
+ console.log(LOG_TAG, '[iOS] webviewInstance=', webviewInstance ? '[Object]' : 'null')
try {
// 实现 WKScriptMessageHandler 协议
const MessageHandler = plus.ios.implements('NSObject<WKScriptMessageHandler>', {
'userContentController:didReceiveScriptMessage:': function (controller, message) {
- console.log('[weizanPay][iOS] messageHandler 被触发, message=', message)
+ console.log(LOG_TAG, '===== [iOS] didReceiveScriptMessage 被调用 =====')
if (!message) {
- console.warn('[weizanPay][iOS] message 为空,终止')
+ console.warn(LOG_TAG, '[iOS] message 为空,跳过')
return
}
// message.name === 'jumpToMiniProgramPay',message.body 是 H5 传的参数
let raw = message
try {
const body = plus.ios.invoke(message, 'body')
- console.log('[weizanPay][iOS] message.body=', body, '类型:', typeof body)
+ console.log(LOG_TAG, '[iOS] message.body=', safeStringify(body))
if (body !== undefined && body !== null) {
raw = body
}
- } catch (e) {
- console.error('[weizanPay][iOS] 取 message.body 失败:', e)
- }
+ } catch (e) {}
+ console.log(LOG_TAG, '[iOS] 原始 raw=', safeStringify(raw))
const params = normalizePayParams(parseJsonSafe(raw))
+ console.log(LOG_TAG, '[iOS] 解析后 params=', safeStringify(params))
if (typeof onPay === 'function') {
try { onPay(params) } catch (e) {}
}
jumpToMiniProgramPay(params)
}
})
+ console.log(LOG_TAG, '[iOS] MessageHandler 已创建:', MessageHandler ? '[Object]' : 'null')
// 取底层 WKWebView:plus.webview 实例 → valueForKey('webView') 或直接复用
+ console.log(LOG_TAG, '[iOS] 尝试 valueForKey:webView')
const wkWebView = safeIosInvoke(webviewInstance, 'valueForKey:', 'webView') || webviewInstance
+ console.log(LOG_TAG, '[iOS] wkWebView=', wkWebView ? '[Object]' : 'null')
const configuration = safeIosInvoke(wkWebView, 'configuration')
+ console.log(LOG_TAG, '[iOS] configuration=', configuration ? '[Object]' : 'null')
if (!configuration) {
- console.error('[weizanPay][iOS] 未拿到 WKWebView configuration')
+ console.error(LOG_TAG, '[iOS] 未拿到 WKWebView configuration,注册中止')
+ console.error(LOG_TAG, '[iOS] 请确认传入的是 plus.webview.create 创建的子 webview')
return
}
const userContentController = safeIosInvoke(configuration, 'userContentController')
+ console.log(LOG_TAG, '[iOS] userContentController=', userContentController ? '[Object]' : 'null')
if (!userContentController) {
- console.error('[weizanPay][iOS] 未拿到 userContentController')
+ console.error(LOG_TAG, '[iOS] 未拿到 userContentController,注册中止')
return
}
plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME)
- console.log('[weizanPay][iOS] JSBridge 已注册')
+ console.log(LOG_TAG, '[iOS] addScriptMessageHandler 完成,window.webkit.messageHandlers.' + PAY_BRIDGE_NAME + ' 已注册')
+ // 验证 H5 端是否能访问到 messageHandlers
+ try {
+ if (typeof webviewInstance.evalJS === 'function') {
+ webviewInstance.evalJS(`
+ try {
+ if (!window.webkit || !window.webkit.messageHandlers) {
+ console.log('[weizanPay][H5] window.webkit.messageHandlers 不存在');
+ } else {
+ var names = Object.keys(window.webkit.messageHandlers);
+ console.log('[weizanPay][H5] messageHandlers names=' + names.join(','));
+ var handler = window.webkit.messageHandlers.${PAY_BRIDGE_NAME};
+ console.log('[weizanPay][H5] ${PAY_BRIDGE_NAME} handler 类型=' + typeof handler);
+ }
+ } catch (e) {
+ console.log('[weizanPay][H5] 检查 messageHandlers 异常: ' + e.message);
+ }
+ `)
+ }
+ } catch (e) {
+ console.warn(LOG_TAG, '[iOS] evalJS 验证失败:', e && e.message)
+ }
} catch (e) {
- console.error('[weizanPay][iOS] 注册失败:', e)
+ console.error(LOG_TAG, '[iOS] 注册 JSBridge 失败:', e)
+ console.error(LOG_TAG, '[iOS] 异常 message=', e && e.message)
}
}
// ===== 辅助函数 =====
/**
+ * 安全序列化任意值,用于日志打印。原生对象、循环引用、undefined 都不会抛错。
+ */
+function safeStringify(value) {
+ try {
+ if (value === undefined) {
+ return 'undefined'
+ }
+ if (value === null) {
+ return 'null'
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value)
+ }
+ if (typeof value === 'function') {
+ return '[Function]'
+ }
+ return JSON.stringify(value)
+ } catch (e) {
+ try {
+ return String(value)
+ } catch (e2) {
+ return '[Unserializable]'
+ }
+ }
+}
+
+/**
* 把 H5 传过来的 params 归一化为标准结构。
* 同时兼容下划线命名(order_id)与驼峰命名(orderId)。
*
--
Gitblit v1.7.1