diff --git a/.gitignore b/.gitignore
index 4f7e0922..ba04b3a0 100755
--- a/.gitignore
+++ b/.gitignore
@@ -19,5 +19,6 @@ uniapp/TASK.md
uniapp/PLANNING.md
niucloud/TASK.md
niucloud/PLANNING.md
+openspec
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..06696994
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,18 @@
+
+# OpenSpec Instructions
+
+These instructions are for AI assistants working in this project.
+
+Always open `@/openspec/AGENTS.md` when the request:
+- Mentions planning or proposals (words like proposal, spec, change, plan)
+- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
+- Sounds ambiguous and you need the authoritative spec before coding
+
+Use `@/openspec/AGENTS.md` to learn:
+- How to create and apply change proposals
+- Spec format and conventions
+- Project structure and guidelines
+
+Keep this managed block so 'openspec update' can refresh the instructions.
+
+
\ No newline at end of file
diff --git a/PROJECT.md b/PROJECT.md
new file mode 100644
index 00000000..e3998edf
--- /dev/null
+++ b/PROJECT.md
@@ -0,0 +1,128 @@
+# 智慧教务系统
+
+## 项目概述
+
+课程预约、消课、服务管理的企业级教务系统,支持学员端和教练端。
+
+## 项目结构
+
+```
+zhjwxt/
+├── admin/ # 管理后台 (Vue3 + Element Plus + TypeScript)
+├── uniapp/ # 移动端客户端 (UniApp + Vue3 + TypeScript)
+├── niucloud/ # ThinkPHP8 后端服务
+├── openspec/ # OpenSpec 规范文档
+├── docker/ # Docker 配置
+├── doc/ # 文档目录
+└── vendor/ # Composer 依赖
+```
+
+## 技术栈
+
+| 模块 | 技术栈 |
+|------|--------|
+| 后端 | ThinkPHP 8.0 + MySQL + Redis |
+| 管理后台 | Vue3 + Element Plus + TypeScript + Vite |
+| 移动端 | UniApp + Vue3 + TypeScript + Pinia |
+
+## 运行环境
+
+### 数据库
+- **数据库名**: `niucloud`
+- **用户名**: `niucloud`
+- **密码**: `niucloud123`
+
+### 服务端口
+- **后端 API**: `http://localhost:20080`
+ - 客户端接口: `/api`
+ - 管理端接口: `/adminapi`
+- **管理后台**: `http://localhost:23000`
+- **移动端 H5**: `http://localhost:8080`
+
+### 启动命令
+```bash
+# 启动所有服务 (Docker)
+./start.sh
+
+# 单独启动后端
+cd niucloud && php think serve
+
+# 启动管理后台
+cd admin && npm run dev
+
+# 启动移动端
+cd uniapp && npm run dev:h5
+```
+
+## 核心功能模块
+
+### 课程管理
+- 课程创建/编辑/删除
+- 课程排期
+- 课程类型管理
+
+### 预约管理
+- 学员预约
+- 预约审核
+- 预约取消/改期
+
+### 消课管理
+- 消课记录
+- 消课统计
+- 消课审核
+
+### 学员管理
+- 学员信息
+- 学员卡管理
+- 学员消费记录
+
+### 教练管理
+- 教练信息
+- 教练排班
+- 教练绩效
+
+## 端点说明
+
+### 移动端目录
+| 目录 | 说明 |
+|------|------|
+| `pages/common/` | 通用页面(登录、设置等) |
+| `pages/student/` | 学员端页面 |
+| `pages/coach/` | 教练端页面 |
+| `pages-coach/` | 教练端专属页面 |
+| `pages-common/` | 通用页面模块 |
+
+### 后端目录
+| 目录 | 说明 |
+|------|------|
+| `app/adminapi/controller/` | 管理后台接口 |
+| `app/api/controller/` | 客户端接口 |
+| `app/service/admin/` | 管理后台业务逻辑 |
+| `app/service/api/` | 客户端业务逻辑 |
+| `app/model/` | 数据模型 |
+
+## 测试账号
+
+### 管理后台
+- **地址**: `http://localhost:23000`
+- **用户名**: `admin`
+- **密码**: `123123`
+
+## 开发规范
+
+### 新增功能流程
+1. 后端:控制器 → Service → Model
+2. 前端:API → 页面组件
+3. 路由:注册新页面路由
+
+### 文件命名
+- 控制器:`Xxx.php`
+- Service:`XxxService.php`
+- Model:`Xxx.php`
+- 页面:`.vue`
+
+## 相关项目
+
+- **钜惠云仓商城项目**: `/Users/mac/coding/juhuiyuncang/juhuiyuncloudadmin`
+- **数据采集项目**: `/Users/mac/coding/juhuiyuncang/datahandle`
+- **物流项目**: `/Users/mac/coding/juhuiyuncang/groupeddelivery`
diff --git a/admin/bug.md b/admin/bug.md
new file mode 100644
index 00000000..e69de29b
diff --git a/admin/src/app/views/login/index.vue b/admin/src/app/views/login/index.vue
index 2e8718b7..456e28c2 100755
--- a/admin/src/app/views/login/index.vue
+++ b/admin/src/app/views/login/index.vue
@@ -200,16 +200,40 @@ const loginFn = (data = {}) => {
loading.value = true
userStore
.login({ username: form.username, password: form.password, ...data })
- .then((res) => {
- const {
- query: { redirect },
- } = route
- const path = typeof redirect === 'string' ? redirect : '/'
- const url = router.resolve(path)
- // console.log(url);
- location.href = '/'
+ .then(async (res) => {
+ try {
+ // 获取用户菜单权限
+ await userStore.getAuthMenusFn()
+
+ // 登录成功,找到第一个有效路由并跳转
+ const {
+ query: { redirect },
+ } = route
+
+ // 如果有重定向路径,优先使用;否则跳转到第一个有效路由
+ if (redirect && typeof redirect === 'string') {
+ console.log('登录成功,跳转到重定向页面:', redirect)
+ await router.push(redirect)
+ } else {
+ // 查找第一个有效的菜单路由
+ const { findFirstValidRoute } = await import('@/router/routers')
+ const firstRoute = findFirstValidRoute(userStore.routers)
+ if (firstRoute) {
+ console.log('登录成功,跳转到第一个有效路由:', firstRoute)
+ await router.push({ name: firstRoute })
+ } else {
+ console.error('未找到有效的菜单路由')
+ throw new Error('未找到有效的菜单路由')
+ }
+ }
+ console.log('跳转完成')
+ } catch (error) {
+ console.error('登录后跳转失败:', error)
+ loading.value = false
+ }
})
- .catch(() => {
+ .catch((err) => {
+ console.error('登录失败:', err)
loading.value = false
})
}
diff --git a/admin/src/router/index.ts b/admin/src/router/index.ts
index 365a3b49..be86633c 100755
--- a/admin/src/router/index.ts
+++ b/admin/src/router/index.ts
@@ -92,12 +92,28 @@ router.beforeEach(async (to: any, from, next) => {
} else if (userStore.token) {
// 如果已加载路由
if (userStore.routers.length) {
+ console.log('路由守卫: 已加载路由数量=', userStore.routers.length, '目标路径=', to.path)
+
if (to.path === loginPath) {
+ console.log('路由守卫: 从登录页跳转到应用根路径')
next(`/${getAppType()}`)
+ } else if (to.path === `/${getAppType()}`) {
+ // 如果访问的是应用根路径,重定向到第一个有效路由
+ const firstRoute = findFirstValidRoute(userStore.routers)
+ console.log('路由守卫: 访问根路径,找到的第一个路由=', firstRoute)
+ if (firstRoute) {
+ console.log('路由守卫: 重定向到第一个路由', { name: firstRoute })
+ next({ name: firstRoute })
+ } else {
+ console.log('路由守卫: 没有找到有效路由')
+ next()
+ }
} else {
+ console.log('路由守卫: 正常访问', to.path)
next()
}
} else {
+ console.log('路由守卫: 未加载路由,开始加载...')
try {
if (!systemStore.apps.length) {
await systemStore.getInstallAddons()
@@ -112,6 +128,16 @@ router.beforeEach(async (to: any, from, next) => {
firstRoute = userStore.addonIndexRoute[systemStore.apps[0].key]
}
+ console.log('路由守卫: 首个路由=', firstRoute, '路由列表=', userStore.routers)
+
+ // 如果没有找到有效路由,使用默认的登录页面或404页面
+ if (!firstRoute) {
+ console.warn('未找到有效的菜单路由,请检查用户权限配置')
+ // 清除token并重新登录
+ userStore.logout()
+ return
+ }
+
ROOT_ROUTER.redirect = { name: firstRoute }
router.addRoute(ROOT_ROUTER)
@@ -129,9 +155,19 @@ router.beforeEach(async (to: any, from, next) => {
// 动态添加可访问路由表
router.addRoute(ADMIN_ROUTE.name, route)
})
- next(to)
+
+ console.log('路由守卫: 动态路由已添加,目标路径=', to.path)
+
+ // 如果访问的是登录页或应用根路径,跳转到第一个有效路由
+ if (to.path === loginPath || to.path === `/${getAppType()}`) {
+ console.log('路由守卫: 跳转到首个路由', { name: firstRoute })
+ next({ name: firstRoute })
+ } else {
+ console.log('路由守卫: 继续访问目标页面', to.path)
+ next(to)
+ }
} catch (err) {
- console.log(err)
+ console.error('路由守卫: 加载路由失败', err)
next({ path: loginPath, query: { redirect: to.fullPath } })
}
}
diff --git a/admin/src/router/routers.ts b/admin/src/router/routers.ts
index bd488681..e26d5635 100755
--- a/admin/src/router/routers.ts
+++ b/admin/src/router/routers.ts
@@ -34,8 +34,7 @@ export const ADMIN_ROUTE: RouteRecordRaw = {
children: [
{
path: '',
- name: Symbol('adminRoot'),
- component: Default,
+ redirect: '/admin/login', // 默认重定向到登录页,登录后会由路由守卫动态修改
},
{
path: 'login',
@@ -142,15 +141,26 @@ export function formatRouters(
export function findFirstValidRoute(
routes: RouteRecordRaw[]
): string | undefined {
+ console.log('findFirstValidRoute: 开始查找,路由数量=', routes.length)
+
for (const route of routes) {
+ console.log('findFirstValidRoute: 检查路由', route.name, 'meta=', route.meta)
+
if (route.meta?.type == 1 && route.meta?.show) {
+ console.log('findFirstValidRoute: 找到有效路由=', route.name)
return route.name as string
}
+
if (route.children) {
+ console.log('findFirstValidRoute: 检查子路由,数量=', route.children.length)
const name = findFirstValidRoute(route.children)
if (name) {
+ console.log('findFirstValidRoute: 在子路由中找到有效路由=', name)
return name
}
}
}
+
+ console.log('findFirstValidRoute: 没有找到有效路由')
+ return undefined
}
diff --git a/admin/src/stores/modules/user.ts b/admin/src/stores/modules/user.ts
index b50294fb..86812a14 100755
--- a/admin/src/stores/modules/user.ts
+++ b/admin/src/stores/modules/user.ts
@@ -60,7 +60,10 @@ const useUserStore = defineStore('user', {
return new Promise((resolve, reject) => {
getAuthMenus({})
.then((res) => {
+ console.log('获取菜单数据:', res.data)
this.routers = formatRouters(res.data)
+ console.log('格式化后的路由:', this.routers)
+
// 获取插件的首个菜单
this.routers.forEach((item, index) => {
if (item.meta.app !== '') {
@@ -73,9 +76,11 @@ const useUserStore = defineStore('user', {
}
}
})
+ console.log('插件首页路由:', this.addonIndexRoute)
resolve(res)
})
.catch((error) => {
+ console.error('获取菜单失败:', error)
reject(error)
})
})
diff --git a/docker-compose.yml b/docker-compose.yml
index 17dc82cf..2cbbf365 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -9,9 +9,6 @@ services:
- ./niucloud:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
- # 时区同步
- - /etc/localtime:/etc/localtime:ro
- - /etc/timezone:/etc/timezone:ro
working_dir: /var/www/html
depends_on:
- mysql
@@ -35,9 +32,6 @@ services:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./docker/logs/nginx:/var/log/nginx
- # 时区同步
- - /etc/localtime:/etc/localtime:ro
- - /etc/timezone:/etc/timezone:ro
environment:
- TZ=Asia/Shanghai
depends_on:
@@ -62,9 +56,6 @@ services:
- ./docker/data/mysql:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./docker/logs/mysql:/var/log/mysql
- # 时区同步
- - /etc/localtime:/etc/localtime:ro
- - /etc/timezone:/etc/timezone:ro
command: --default-authentication-plugin=mysql_native_password
networks:
- niucloud_network
diff --git a/niucloud/app/adminapi/controller/auth/Auth.php b/niucloud/app/adminapi/controller/auth/Auth.php
index 8f4748ce..fbc01bbc 100755
--- a/niucloud/app/adminapi/controller/auth/Auth.php
+++ b/niucloud/app/adminapi/controller/auth/Auth.php
@@ -57,6 +57,21 @@ class Auth extends BaseAdminController
return success('MODIFY_SUCCESS');
}
+ /**
+ * 测试接口 - 用于调试
+ * @return Response
+ */
+ public function test()
+ {
+ $data = [
+ 'uid' => $this->request->uid(),
+ 'username' => $this->request->username(),
+ 'token' => $this->request->adminToken(),
+ 'message' => 'Token验证成功'
+ ];
+ return success($data);
+ }
+
/**
* 更新用户
*/
diff --git a/niucloud/app/adminapi/middleware/AdminCheckToken.php b/niucloud/app/adminapi/middleware/AdminCheckToken.php
index e6413bb4..fefb75d3 100755
--- a/niucloud/app/adminapi/middleware/AdminCheckToken.php
+++ b/niucloud/app/adminapi/middleware/AdminCheckToken.php
@@ -26,9 +26,20 @@ class AdminCheckToken
{
//通过配置来设置系统header参数
$token = $request->adminToken();
- $token_info = (new LoginService())->parseToken($token);
- $request->uid($token_info['uid']);
- $request->username($token_info['username']);
- return $next($request);
+
+ if (empty($token)) {
+ throw new \core\exception\AuthException('MUST_LOGIN', 401);
+ }
+
+ try {
+ $token_info = (new LoginService())->parseToken($token);
+ $request->uid($token_info['uid']);
+ $request->username($token_info['username']);
+ return $next($request);
+ } catch (\Throwable $e) {
+ // 记录错误日志以便调试
+ \think\facade\Log::error('Token验证失败: ' . $e->getMessage());
+ throw new \core\exception\AuthException('LOGIN_EXPIRE', 401);
+ }
}
}
diff --git a/niucloud/app/adminapi/route/auth.php b/niucloud/app/adminapi/route/auth.php
index 61af423d..d174db88 100755
--- a/niucloud/app/adminapi/route/auth.php
+++ b/niucloud/app/adminapi/route/auth.php
@@ -30,6 +30,9 @@ Route::group('auth', function () {
Route::put('edit', 'auth.Auth/edit');
//授权用户信息
Route::put('modify/:field', 'auth.Auth/modify');
+
+ // 测试接口 - 仅用于调试token验证问题
+ Route::get('test', 'auth.Auth/test');
})->middleware([
AdminCheckToken::class,
AdminCheckRole::class,
diff --git a/niucloud/app/model/document/DocumentGenerateLog.php b/niucloud/app/model/document/DocumentGenerateLog.php
index c7437237..75a49570 100755
--- a/niucloud/app/model/document/DocumentGenerateLog.php
+++ b/niucloud/app/model/document/DocumentGenerateLog.php
@@ -12,14 +12,6 @@ class DocumentGenerateLog extends BaseModel
{
protected $pk = 'id';
protected $name = 'document_generate_log';
-
- // 字段类型定义
- protected $type = [
- 'created_at' => 'timestamp',
- 'updated_at' => 'timestamp',
- 'process_start_time' => 'datetime',
- 'process_end_time' => 'datetime'
- ];
/**
* 关联合同表
diff --git a/niucloud/app/service/admin/document/DocumentTemplateService.php b/niucloud/app/service/admin/document/DocumentTemplateService.php
index 45cd3c45..3fbd0fc7 100755
--- a/niucloud/app/service/admin/document/DocumentTemplateService.php
+++ b/niucloud/app/service/admin/document/DocumentTemplateService.php
@@ -83,13 +83,11 @@ class DocumentTemplateService extends BaseAdminService
$info['placeholders'] = $info['placeholders'] ? json_decode($info['placeholders'], true) : [];
$info['file_size_formatted'] = $this->formatFileSize($info['file_size']);
- // 基于placeholder_config构建数据源配置,不查询school_document_data_source_config表
+ // 获取数据源配置信息,从placeholder_config字段获取
$dataSourceConfigs = [];
- if (!empty($info['placeholders'])) {
- foreach ($info['placeholders'] as $placeholder) {
- // 使用placeholder_config中的配置,如果没有则使用默认配置
- $config = $info['placeholder_config'][$placeholder] ?? [];
-
+ if (!empty($info['placeholder_config'])) {
+ // 转换placeholder_config格式为前端需要的data_source_configs格式
+ foreach ($info['placeholder_config'] as $placeholder => $config) {
$dataSourceConfigs[] = [
'id' => 0,
'placeholder' => $placeholder,
@@ -107,6 +105,27 @@ class DocumentTemplateService extends BaseAdminService
}
$info['data_source_configs'] = $dataSourceConfigs;
+
+ // 如果没有数据源配置,但有占位符,则创建默认配置
+ if (empty($dataSourceConfigs) && !empty($info['placeholders'])) {
+ $defaultConfigs = [];
+ foreach ($info['placeholders'] as $placeholder) {
+ $defaultConfigs[] = [
+ 'id' => 0,
+ 'placeholder' => $placeholder,
+ 'data_type' => 'user_input',
+ 'table_name' => '',
+ 'field_name' => '',
+ 'system_function' => '',
+ 'user_input_value' => '',
+ 'sign_party' => '',
+ 'field_type' => 'text',
+ 'is_required' => 0,
+ 'default_value' => ''
+ ];
+ }
+ $info['data_source_configs'] = $defaultConfigs;
+ }
}
return $info;
@@ -145,14 +164,11 @@ class DocumentTemplateService extends BaseAdminService
$insertData[] = [
'contract_id' => $contractId,
'placeholder' => $config['placeholder'],
- 'data_type' => $config['data_type'] ?? 'user_input',
'table_name' => $config['table_name'] ?? '',
'field_name' => $config['field_name'] ?? '',
- 'field_type' => $config['field_type'] ?? 'text',
+ 'field_type' => $config['field_type'] ?? 'string',
'is_required' => $config['is_required'] ?? 0,
'default_value' => $config['default_value'] ?? '',
- 'system_function' => $config['system_function'] ?? '',
- 'sign_party' => $config['sign_party'] ?? '',
'created_at' => date('Y-m-d H:i:s')
];
}
@@ -417,17 +433,12 @@ class DocumentTemplateService extends BaseAdminService
try {
// 1. 保存配置到合同表的placeholder_config字段(保持兼容性)
$template->placeholder_config = json_encode($configData);
-
- // 2. 更新placeholder字段为占位符的逗号分隔文本
- $placeholders = array_keys($configData);
- $template->placeholder = implode(',', $placeholders);
-
- // 3. 更新placeholders字段为JSON数组
- $template->placeholders = json_encode($placeholders);
-
$template->updated_at = date('Y-m-d H:i:s');
$template->save();
+ // 2. 同时保存到独立的数据源配置表(用户期望的表)
+ $this->saveConfigToDataSourceTable($templateId, $configData);
+
\think\facade\Db::commit();
return true;
} catch (\Exception $e) {
@@ -641,19 +652,7 @@ class DocumentTemplateService extends BaseAdminService
// 准备填充数据
$placeholderConfig = json_decode($template['placeholder_config'], true);
- // 检查是否传递了 use_direct_values 参数,如果是则直接使用传递的值
- if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) {
- // 直接使用传递的 fill_data 作为填充值,不进行二次处理
- $fillValues = $data['fill_data'];
- Log::info('使用直接填充模式', [
- 'template_id' => $data['template_id'],
- 'fill_data_count' => count($fillValues),
- 'fill_data_keys' => array_keys($fillValues)
- ]);
- } else {
- // 使用原有的配置处理模式
- $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
- }
+ $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
// 生成文档
$templatePath = public_path() . '/upload/' . $template['contract_template'];
@@ -696,14 +695,12 @@ class DocumentTemplateService extends BaseAdminService
throw new \Exception('无法创建临时文档存储目录,请检查系统权限');
}
- // 预处理:修复被格式化分割的占位符
- $fixedTemplatePath = $this->fixBrokenPlaceholders($templatePath);
-
// 使用 PhpWord 模板处理器
- $templateProcessor = new TemplateProcessor($fixedTemplatePath);
+ $templateProcessor = new TemplateProcessor($templatePath);
- // 智能处理占位符,根据类型使用不同的处理方法
- $this->processPlaceholders($templateProcessor, $fillValues, $placeholderConfig);
+ foreach ($fillValues as $placeholder => $value) {
+ $templateProcessor->setValue($placeholder, $value);
+ }
$templateProcessor->saveAs($fullOutputPath);
@@ -737,36 +734,20 @@ class DocumentTemplateService extends BaseAdminService
'download_url' => url('/upload/' . $outputPath)
];
} else {
- // 复制失败,使用runtime目录作为替代方案
- $runtimeDir = runtime_path() . 'generated_documents' . DIRECTORY_SEPARATOR;
- if (!is_dir($runtimeDir)) {
- @mkdir($runtimeDir, 0755, true);
- }
-
- $runtimePath = $runtimeDir . $outputFileName;
- $runtimeCopySuccess = false;
-
- // 尝试复制到runtime目录
- if (@copy($fullOutputPath, $runtimePath)) {
- $runtimeCopySuccess = true;
- @unlink($fullOutputPath); // 删除临时文件
- }
-
- // 更新生成记录
+ // 复制失败,保留临时文件,提供临时下载路径
$log->status = 'completed';
- $log->generated_file_path = $runtimeCopySuccess ? 'runtime/generated_documents/' . $outputFileName : 'temp/' . $outputFileName;
+ $log->generated_file_path = 'temp/' . $outputFileName;
$log->generated_file_name = $outputFileName;
- $log->temp_file_path = $runtimeCopySuccess ? $runtimePath : $fullOutputPath;
$log->process_end_time = date('Y-m-d H:i:s');
$log->save();
return [
'log_id' => $log->id,
- 'file_path' => $runtimeCopySuccess ? 'runtime/generated_documents/' . $outputFileName : 'temp/' . $outputFileName,
+ 'file_path' => 'temp/' . $outputFileName,
'file_name' => $outputFileName,
- 'download_url' => url('/adminapi/document/download/' . $log->id),
- 'temp_file_path' => $runtimeCopySuccess ? $runtimePath : $fullOutputPath,
- 'message' => $runtimeCopySuccess ? '文档已生成,使用临时下载链接' : '文档已生成,但无法复制到公共目录,请联系管理员处理权限问题'
+ 'download_url' => '',
+ 'temp_file_path' => $fullOutputPath,
+ 'message' => '文档已生成,但无法复制到公共目录,请联系管理员处理权限问题'
];
}
@@ -852,37 +833,13 @@ class DocumentTemplateService extends BaseAdminService
$tableName = $config['table_name'];
$fieldName = $config['field_name'];
- // 改进的数据库查询逻辑,支持条件查询
+ // 简单的数据库查询(实际应用中需要更完善的查询逻辑)
$model = \think\facade\Db::connect();
- $query = $model->table($tableName);
-
- // 如果有传入的查询条件(比如学员ID),使用条件查询
- if (!empty($userFillData) && is_array($userFillData)) {
- foreach ($userFillData as $key => $value) {
- // 支持常见的查询字段
- if (in_array($key, ['id', 'student_id', 'user_id', 'person_id', 'contract_id'])) {
- $query->where($key, $value);
- break; // 只使用第一个匹配的条件
- }
- }
- }
-
- $result = $query->field($fieldName)->find();
-
- Log::info('数据库查询', [
- 'table' => $tableName,
- 'field' => $fieldName,
- 'result' => $result,
- 'user_data' => $userFillData
- ]);
+ $result = $model->table($tableName)->field($fieldName)->find();
return $result[$fieldName] ?? $config['default_value'] ?? '';
} catch (\Exception $e) {
- Log::error('数据库查询失败:' . $e->getMessage(), [
- 'table' => $config['table_name'] ?? '',
- 'field' => $config['field_name'] ?? '',
- 'config' => $config
- ]);
+ Log::error('数据库查询失败:' . $e->getMessage());
return $config['default_value'] ?? '';
}
}
@@ -1012,24 +969,9 @@ class DocumentTemplateService extends BaseAdminService
throw new \Exception('文档尚未生成完成');
}
- // 优先尝试从public目录下载
$filePath = public_path() . '/upload/' . $log['generated_file_path'];
-
- // 如果public目录文件不存在,尝试从临时文件路径下载
- if (!file_exists($filePath) && !empty($log['temp_file_path'])) {
- $filePath = $log['temp_file_path'];
- }
-
- // 如果还是不存在,尝试从runtime目录
- if (!file_exists($filePath)) {
- $runtimePath = runtime_path() . 'generated_documents' . DIRECTORY_SEPARATOR . $log['generated_file_name'];
- if (file_exists($runtimePath)) {
- $filePath = $runtimePath;
- }
- }
-
if (!file_exists($filePath)) {
- throw new \Exception('文件不存在或已被删除');
+ throw new \Exception('文件不存在');
}
// 更新下载统计
@@ -1069,130 +1011,6 @@ class DocumentTemplateService extends BaseAdminService
return $this->pageQuery($searchModel);
}
- /**
- * 修复被Word格式化分割的占位符
- * Word在编辑时会在占位符中插入格式化标签,导致{{placeholder}}被分割
- * 这个方法会创建一个修复后的临时文件
- *
- * @param string $templatePath 原始模板文件路径
- * @return string 修复后的模板文件路径
- */
- private function fixBrokenPlaceholders(string $templatePath)
- {
- try {
- // 创建临时文件
- $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_templates';
- if (!is_dir($tempDir)) {
- mkdir($tempDir, 0755, true);
- }
-
- $fixedTemplatePath = $tempDir . DIRECTORY_SEPARATOR . 'fixed_' . basename($templatePath);
-
- // 复制原文件到临时位置
- if (!copy($templatePath, $fixedTemplatePath)) {
- Log::warning('无法创建临时模板文件,使用原始模板', ['template_path' => $templatePath]);
- return $templatePath;
- }
-
- // 打开ZIP文件进行修复
- $zip = new \ZipArchive();
-
- if ($zip->open($fixedTemplatePath) === TRUE) {
- $content = $zip->getFromName('word/document.xml');
-
- if ($content !== false) {
- $originalLength = strlen($content);
- $fixedContent = $content;
-
- // 第一步:修复跨XML标签的占位符(最常见的模式)
- // 模式1:修复 {{...内容}} 这种分割
- $pattern1 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/';
- $fixedContent = preg_replace_callback($pattern1, function($matches) {
- $placeholder = '{{' . $matches[1] . $matches[2] . '}}';
- return '' . $placeholder . '';
- }, $fixedContent);
-
- // 模式2:修复 {...{内容}} 这种分割
- $pattern2 = '/]*>\{<\/w:t>([^<]*)]*>\{([^<]*)\}\}<\/w:t>/';
- $fixedContent = preg_replace_callback($pattern2, function($matches) {
- $placeholder = '{{' . $matches[1] . $matches[2] . '}}';
- return '' . $placeholder . '';
- }, $fixedContent);
-
- // 模式3:修复三段式分割 {{...中间...}}
- $pattern3 = '/]*>\{\{<\/w:t>([^<]*)]*>([^<]*)<\/w:t>([^<]*)]*>([^<]*)\}\}<\/w:t>/';
- $fixedContent = preg_replace_callback($pattern3, function($matches) {
- $placeholder = '{{' . $matches[1] . $matches[2] . $matches[4] . '}}';
- return '' . $placeholder . '';
- }, $fixedContent);
-
- // 第二步:处理更复杂的格式化分割(包含rPr标签的)
- // 匹配包含格式化信息的分割模式
- $complexPattern = '/\{\{[^}]*?(?:<\/w:t><\/w:r>]*?>(?:]*?>.*?<\/w:rPr>)?]*?>)[^}]*?\}\}/s';
- $fixedContent = preg_replace_callback($complexPattern, function($matches) {
- $placeholder = $matches[0];
- // 移除所有XML标签,保留纯文本
- $cleaned = preg_replace('/<[^>]*?>/', '', $placeholder);
- // 验证是否为有效占位符
- if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) {
- return $placeholder;
- }
- // 保持原有的第一个w:t标签结构
- if (preg_match('/^([^<]*]*>)/', $placeholder, $tagMatch)) {
- return $tagMatch[1] . $cleaned . '';
- }
- return $cleaned;
- }, $fixedContent);
-
- // 第三步:修复任何剩余的基本分割模式
- $basicPattern = '/\{\{[^}]*?<[^>]*?>[^}]*?\}\}/';
- $fixedContent = preg_replace_callback($basicPattern, function($matches) {
- $placeholder = $matches[0];
- $cleaned = preg_replace('/<[^>]*?>/', '', $placeholder);
- if (!preg_match('/^\{\{[^}]+\}\}$/', $cleaned)) {
- return $placeholder;
- }
- return $cleaned;
- }, $fixedContent);
-
- // 将修复后的内容写回ZIP文件
- if ($zip->addFromString('word/document.xml', $fixedContent)) {
- $zip->close();
-
- Log::info('占位符修复完成', [
- 'template_file' => $templatePath,
- 'fixed_file' => $fixedTemplatePath,
- 'original_length' => $originalLength,
- 'fixed_length' => strlen($fixedContent),
- 'size_change' => strlen($fixedContent) - $originalLength
- ]);
-
- return $fixedTemplatePath;
- } else {
- $zip->close();
- Log::warning('无法写入修复后的内容,使用原始模板', ['template_path' => $templatePath]);
- return $templatePath;
- }
- } else {
- $zip->close();
- Log::warning('无法读取document.xml,使用原始模板', ['template_path' => $templatePath]);
- return $templatePath;
- }
- } else {
- Log::warning('无法打开Word文档,使用原始模板', ['template_path' => $templatePath]);
- return $templatePath;
- }
- } catch (\Exception $e) {
- Log::error('修复占位符失败', [
- 'template_path' => $templatePath,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- // 修复失败不影响主流程,返回原始模板路径
- return $templatePath;
- }
- }
-
/**
* 格式化文件大小
* @param int $size
@@ -1432,15 +1250,10 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置
if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig);
-
- // 更新placeholder字段为占位符的逗号分隔文本
- $placeholders = array_keys($newConfig);
- $template->placeholder = implode(',', $placeholders);
-
- // 更新placeholders字段为JSON数组
- $template->placeholders = json_encode($placeholders);
-
$template->save();
+
+ // 同步更新数据源配置表
+ $this->saveConfigToDataSourceTable($id, $newConfig);
}
return [
@@ -1563,15 +1376,10 @@ class DocumentTemplateService extends BaseAdminService
// 更新占位符配置
if (!empty($newConfig)) {
$template->placeholder_config = json_encode($newConfig);
-
- // 更新placeholder字段为占位符的逗号分隔文本
- $placeholders = array_keys($newConfig);
- $template->placeholder = implode(',', $placeholders);
-
- // 更新placeholders字段为JSON数组
- $template->placeholders = json_encode($placeholders);
-
$template->save();
+
+ // 同步更新数据源配置表
+ $this->saveConfigToDataSourceTable($id, $newConfig);
}
return [
@@ -1592,1214 +1400,4 @@ class DocumentTemplateService extends BaseAdminService
throw new \Exception('模板文档更新失败:' . $e->getMessage());
}
}
-
- /**
- * 使用XML字符串操作生成Word文档
- * 此方法直接读取Word模板的XML内容,进行字符串替换,然后保存为新的Word文档
- * @param array $data
- * @return array
- * @throws \Exception
- */
- public function generateDocumentByXmlString(array $data)
- {
- $template = $this->contractModel->find($data['template_id']);
- if (!$template) {
- throw new \Exception('模板不存在');
- }
-
- if (empty($template['placeholder_config'])) {
- throw new \Exception('模板尚未配置占位符');
- }
-
- // 创建生成记录
- $logData = [
- 'site_id' => $this->site_id,
- 'template_id' => $data['template_id'],
- 'user_id' => $this->uid,
- 'user_type' => 1,
- 'fill_data' => json_encode($data['fill_data']),
- 'status' => 'pending',
- 'completed_at' => date('Y-m-d H:i:s')
- ];
-
- $log = $this->logModel->create($logData);
-
- try {
- // 更新状态为处理中
- $log->status = 'processing';
- $log->process_start_time = date('Y-m-d H:i:s');
- $log->save();
-
- // 准备填充数据
- $placeholderConfig = json_decode($template['placeholder_config'], true);
- if (!empty($data['use_direct_values']) && $data['use_direct_values'] === true) {
- $fillValues = $data['fill_data'];
- Log::info('使用直接填充模式(XML字符串方法)', [
- 'template_id' => $data['template_id'],
- 'fill_data_count' => count($fillValues),
- 'fill_data_keys' => array_keys($fillValues)
- ]);
- } else {
- $fillValues = $this->prepareFillData($placeholderConfig, $data['fill_data']);
- }
-
- // 原始模板路径
- $templatePath = public_path() . '/upload/' . $template['contract_template'];
- $outputFileName = $data['output_filename'] ?: ($template['contract_name'] . '_' . date('YmdHis') . '.docx');
-
- // 生成输出路径
- $outputPath = 'generated_documents/' . date('Y/m/') . $outputFileName;
- $publicOutputPath = public_path() . '/upload/' . $outputPath;
- $publicOutputDir = dirname($publicOutputPath);
-
- // 确保输出目录存在
- if (!is_dir($publicOutputDir)) {
- if (!mkdir($publicOutputDir, 0755, true) && !is_dir($publicOutputDir)) {
- throw new \Exception('无法创建输出目录:' . $publicOutputDir);
- }
- }
-
- // 使用XML字符串方法生成文档
- $success = $this->processWordDocumentXml($templatePath, $publicOutputPath, $fillValues);
-
- if (!$success) {
- throw new \Exception('XML字符串处理失败');
- }
-
- // 更新生成记录
- $log->status = 'completed';
- $log->generated_file_path = $outputPath;
- $log->generated_file_name = $outputFileName;
- $log->process_end_time = date('Y-m-d H:i:s');
- $log->save();
-
- Log::info('XML字符串方法生成Word文档成功', [
- 'template_id' => $data['template_id'],
- 'output_path' => $outputPath,
- 'fill_values_count' => count($fillValues)
- ]);
-
- return [
- 'log_id' => $log->id,
- 'file_path' => $outputPath,
- 'file_name' => $outputFileName,
- 'download_url' => url('/upload/' . $outputPath)
- ];
-
- } catch (\Exception $e) {
- // 更新记录为失败状态
- $log->status = 'failed';
- $log->error_msg = $e->getMessage();
- $log->process_end_time = date('Y-m-d H:i:s');
- $log->save();
-
- Log::error('XML字符串方法生成Word文档失败', [
- 'template_id' => $data['template_id'],
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- throw new \Exception('文档生成失败:' . $e->getMessage());
- }
- }
-
- /**
- * 处理Word文档的XML内容进行占位符替换
- * @param string $templatePath 模板文件路径
- * @param string $outputPath 输出文件路径
- * @param array $fillValues 填充值数组
- * @return bool
- * @throws \Exception
- */
- private function processWordDocumentXml(string $templatePath, string $outputPath, array $fillValues): bool
- {
- try {
- // 检查模板文件是否存在
- if (!file_exists($templatePath)) {
- throw new \Exception('模板文件不存在:' . $templatePath);
- }
-
- // 创建临时工作目录
- $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_processing_' . uniqid();
- if (!mkdir($tempDir, 0755, true)) {
- throw new \Exception('无法创建临时工作目录');
- }
-
- try {
- // 1. 解压Word文档到临时目录
- $zip = new \ZipArchive();
- $result = $zip->open($templatePath);
-
- if ($result !== TRUE) {
- throw new \Exception('无法打开Word模板文件,错误代码:' . $result);
- }
-
- if (!$zip->extractTo($tempDir)) {
- $zip->close();
- throw new \Exception('无法解压Word模板文件');
- }
- $zip->close();
-
- // 2. 读取document.xml文件
- $documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml';
- if (!file_exists($documentXmlPath)) {
- throw new \Exception('Word文档缺少document.xml文件');
- }
-
- $xmlContent = file_get_contents($documentXmlPath);
- if ($xmlContent === false) {
- throw new \Exception('无法读取document.xml文件内容');
- }
-
- Log::info('读取XML内容成功', [
- 'original_length' => strlen($xmlContent),
- 'placeholders_to_replace' => array_keys($fillValues)
- ]);
-
- // 3. 进行占位符替换
- $modifiedXmlContent = $this->replaceXmlPlaceholders($xmlContent, $fillValues);
-
- Log::info('XML占位符替换完成', [
- 'original_length' => strlen($xmlContent),
- 'modified_length' => strlen($modifiedXmlContent),
- 'size_change' => strlen($modifiedXmlContent) - strlen($xmlContent)
- ]);
-
- // 4. 写回修改后的document.xml
- if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) {
- throw new \Exception('无法写入修改后的document.xml');
- }
-
- // 5. 重新打包为Word文档
- $newZip = new \ZipArchive();
- $result = $newZip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
-
- if ($result !== TRUE) {
- throw new \Exception('无法创建输出Word文件,错误代码:' . $result);
- }
-
- // 递归添加所有文件到ZIP
- $this->addDirectoryToZip($newZip, $tempDir, '');
-
- if (!$newZip->close()) {
- throw new \Exception('无法保存输出Word文件');
- }
-
- Log::info('Word文档重新打包成功', [
- 'output_path' => $outputPath,
- 'file_size' => filesize($outputPath)
- ]);
-
- // 处理文档中的URL图片(第二次处理)
- $this->processUrlImagesInDocument($outputPath, $fillValues);
-
- return true;
-
- } finally {
- // 清理临时目录
- $this->removeDirectory($tempDir);
- }
-
- } catch (\Exception $e) {
- Log::error('处理Word文档XML失败', [
- 'template_path' => $templatePath,
- 'output_path' => $outputPath,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- }
-
- /**
- * 在XML内容中替换占位符
- * @param string $xmlContent 原始XML内容
- * @param array $fillValues 替换值数组
- * @return string 替换后的XML内容
- */
- private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string
- {
- $modifiedContent = $xmlContent;
-
- // 首先清理被格式化标签分割的占位符
- $modifiedContent = $this->cleanBrokenPlaceholdersInXml($modifiedContent);
-
- $replacementCount = 0;
-
- foreach ($fillValues as $placeholder => $value) {
- // 使用正则表达式匹配占位符,支持空格、换行符、制表符
- // 模式:{{ + 任意空白字符 + placeholder + 任意空白字符 + }}
- $escapedPlaceholder = preg_quote($placeholder, '/');
- $searchPattern = '/\{\{\s*' . $escapedPlaceholder . '\s*\}\}/';
-
- // 转义特殊字符,确保安全的XML替换
- $safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8');
-
- // 使用正则替换
- $beforeLength = strlen($modifiedContent);
- $modifiedContent = preg_replace($searchPattern, $safeValue, $modifiedContent, -1, $count);
- $afterLength = strlen($modifiedContent);
-
- if ($count > 0) {
- $replacementCount += $count;
- Log::info('占位符替换成功', [
- 'placeholder' => $placeholder,
- 'value' => $value,
- 'safe_value' => $safeValue,
- 'replacement_count' => $count,
- 'content_length_change' => $afterLength - $beforeLength
- ]);
- } else {
- Log::warning('占位符未找到', [
- 'placeholder' => $placeholder,
- 'search_pattern' => $searchPattern
- ]);
- }
- }
-
- Log::info('所有占位符处理完成', [
- 'total_replacements' => $replacementCount,
- 'processed_placeholders' => count($fillValues)
- ]);
-
- return $modifiedContent;
- }
-
- /**
- * 清理XML中被格式化标签分割的占位符
- * @param string $xmlContent
- * @return string
- */
- private function cleanBrokenPlaceholdersInXml(string $xmlContent): string
- {
- $cleanedContent = $xmlContent;
-
- Log::info('开始清理分割的占位符');
-
- // 多阶段清理策略:从简单到复杂
-
- // 第一阶段:处理完整的双大括号分割情况
- $cleanedContent = $this->fixCompleteBracketSplits($cleanedContent);
-
- // 第二阶段:处理单个大括号分割情况
- $cleanedContent = $this->fixSingleBracketSplits($cleanedContent);
-
- // 第三阶段:通用清理,移除占位符内部的XML标签
- $cleanedContent = $this->generalPlaceholderCleanup($cleanedContent);
-
- // 第四阶段:最终验证和修复
- $cleanedContent = $this->finalPlaceholderValidation($cleanedContent);
-
- Log::info('占位符清理完成');
-
- return $cleanedContent;
- }
-
- /**
- * 修复完整双大括号被分割的情况
- * @param string $content
- * @return string
- */
- private function fixCompleteBracketSplits(string $content): string
- {
- Log::info('阶段1:修复完整双大括号分割');
-
- $patterns = [
- // 模式1: {{...内容}}
- '/\{\{<\/w:t>.*?]*>([^<]*)\}\}/',
-
- // 模式2: {{...内容}}
- '/\{\{<\/w:t><\/w:r>]*>.*?]*>([^<]*)\}\}/',
-
- // 模式3: 包含rPr格式标签的复杂嵌套
- '/\{\{<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>([^<]*)\}\}/'
- ];
-
- foreach ($patterns as $index => $pattern) {
- $content = preg_replace_callback($pattern, function($matches) use ($index) {
- $placeholderContent = $matches[1];
- $result = '{{' . $placeholderContent . '}}';
-
- Log::info('修复完整双大括号分割', [
- 'pattern_index' => $index + 1,
- 'original' => substr($matches[0], 0, 100) . '...',
- 'fixed' => $result,
- 'placeholder_content' => $placeholderContent
- ]);
-
- return $result;
- }, $content);
- }
-
- return $content;
- }
-
- /**
- * 修复单个大括号被分割的情况
- * @param string $content
- * @return string
- */
- private function fixSingleBracketSplits(string $content): string
- {
- Log::info('阶段2:修复单个大括号分割');
-
- // 处理左大括号被分割:{...{内容}}
- $leftBracketPatterns = [
- // 标准模式:{...{内容}}
- '/\{<\/w:t><\/w:r>]*>.*?]*>\{([^}]*)\}\}/',
-
- // 包含rPr的复杂模式
- '/\{<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>\{([^}]*)\}\}/',
-
- // 简单模式:{...{内容}}
- '/\{<\/w:t>.*?]*>\{([^}]*)\}\}/'
- ];
-
- foreach ($leftBracketPatterns as $index => $pattern) {
- $content = preg_replace_callback($pattern, function($matches) use ($index) {
- $placeholderContent = $matches[1];
- $result = '{{' . $placeholderContent . '}}';
-
- Log::info('修复左大括号分割', [
- 'pattern_index' => $index + 1,
- 'original' => substr($matches[0], 0, 100) . '...',
- 'fixed' => $result,
- 'placeholder_content' => $placeholderContent
- ]);
-
- return $result;
- }, $content);
- }
-
- // 处理右大括号被分割:{{内容}...}
- $rightBracketPatterns = [
- // 标准模式:{{内容}...}
- '/\{\{([^}]*)\}<\/w:t><\/w:r>]*>.*?]*>\}/',
-
- // 包含rPr的复杂模式
- '/\{\{([^}]*)\}<\/w:t><\/w:r>]*>.*?<\/w:rPr>]*>\}/',
-
- // 简单模式:{{内容}...}
- '/\{\{([^}]*)\}<\/w:t>.*?]*>\}/'
- ];
-
- foreach ($rightBracketPatterns as $index => $pattern) {
- $content = preg_replace_callback($pattern, function($matches) use ($index) {
- $placeholderContent = $matches[1];
- $result = '{{' . $placeholderContent . '}}';
-
- Log::info('修复右大括号分割', [
- 'pattern_index' => $index + 1,
- 'original' => substr($matches[0], 0, 100) . '...',
- 'fixed' => $result,
- 'placeholder_content' => $placeholderContent
- ]);
-
- return $result;
- }, $content);
- }
-
- return $content;
- }
-
- /**
- * 通用占位符清理:移除占位符内部的XML标签
- * @param string $content
- * @return string
- */
- private function generalPlaceholderCleanup(string $content): string
- {
- Log::info('阶段3:通用占位符清理');
-
- // 匹配并清理占位符内部的XML标签
- $generalPattern = '/\{\{([^}]*?)<[^>]*?>([^}]*?)\}\}/';
-
- $iterations = 0;
- $maxIterations = 10; // 防止无限循环
-
- do {
- $beforeLength = strlen($content);
- $iterations++;
-
- $content = preg_replace_callback($generalPattern, function($matches) {
- // 移除XML标签,只保留纯文本
- $content = $matches[1] . $matches[2];
- $cleanContent = preg_replace('/<[^>]*?>/', '', $content);
- $result = '{{' . $cleanContent . '}}';
-
- Log::info('通用占位符清理', [
- 'original' => substr($matches[0], 0, 50) . '...',
- 'cleaned' => $result,
- 'content' => $cleanContent
- ]);
-
- return $result;
- }, $content);
-
- $afterLength = strlen($content);
-
- Log::info('通用清理迭代', [
- 'iteration' => $iterations,
- 'length_change' => $afterLength - $beforeLength,
- 'has_more_matches' => preg_match($generalPattern, $content)
- ]);
-
- } while ($beforeLength !== $afterLength &&
- preg_match($generalPattern, $content) &&
- $iterations < $maxIterations);
-
- return $content;
- }
-
- /**
- * 最终占位符验证和修复
- * @param string $content
- * @return string
- */
- private function finalPlaceholderValidation(string $content): string
- {
- Log::info('阶段4:最终验证和修复');
-
- // 查找所有可能的占位符模式
- if (preg_match_all('/\{[^{}]*\}/', $content, $matches)) {
- foreach ($matches[0] as $match) {
- // 检查是否为不完整的占位符(只有一个大括号)
- if (preg_match('/^\{[^{}]+\}$/', $match)) {
- // 查看前后文是否有对应的大括号
- $singleBracketPattern = '/' . preg_quote($match, '/') . '/';
-
- // 尝试修复为双大括号
- $possibleFix = '{' . $match . '}';
-
- Log::info('发现可能的不完整占位符', [
- 'found' => $match,
- 'possible_fix' => $possibleFix
- ]);
- }
- }
- }
-
- // 查找并统计最终的占位符数量
- $finalPlaceholders = [];
- if (preg_match_all('/\{\{([^}]+)\}\}/', $content, $matches)) {
- $finalPlaceholders = $matches[1];
- }
-
- Log::info('最终占位符统计', [
- 'count' => count($finalPlaceholders),
- 'placeholders' => $finalPlaceholders
- ]);
-
- return $content;
- }
-
- /**
- * 递归添加目录到ZIP文件
- * @param \ZipArchive $zip ZIP文件对象
- * @param string $sourcePath 源目录路径
- * @param string $relativePath ZIP内的相对路径
- * @return void
- */
- private function addDirectoryToZip(\ZipArchive $zip, string $sourcePath, string $relativePath): void
- {
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS),
- \RecursiveIteratorIterator::SELF_FIRST
- );
-
- foreach ($iterator as $file) {
- $filePath = $file->getRealPath();
- $relativeName = $relativePath . substr($filePath, strlen($sourcePath) + 1);
-
- // 在Windows系统中统一使用正斜杠
- $relativeName = str_replace('\\', '/', $relativeName);
-
- if ($file->isDir()) {
- // 添加目录
- $zip->addEmptyDir($relativeName);
- } else {
- // 添加文件
- $zip->addFile($filePath, $relativeName);
- }
- }
- }
-
- /**
- * 递归删除目录
- * @param string $dir 要删除的目录路径
- * @return void
- */
- private function removeDirectory(string $dir): void
- {
- if (!is_dir($dir)) {
- return;
- }
-
- $files = array_diff(scandir($dir), ['.', '..']);
- foreach ($files as $file) {
- $filePath = $dir . DIRECTORY_SEPARATOR . $file;
- if (is_dir($filePath)) {
- $this->removeDirectory($filePath);
- } else {
- unlink($filePath);
- }
- }
- rmdir($dir);
- }
-
- /**
- * 智能处理占位符,根据类型使用不同的处理方法
- * @param TemplateProcessor $templateProcessor
- * @param array $fillValues
- * @param array $placeholderConfig
- * @return void
- */
- private function processPlaceholders(TemplateProcessor $templateProcessor, array $fillValues, array $placeholderConfig)
- {
- // 如果没有配置信息,使用简单的文本替换
- if (empty($placeholderConfig)) {
- foreach ($fillValues as $placeholder => $value) {
- $templateProcessor->setValue($placeholder, $value);
- }
- return;
- }
-
- $placeholderConfig = is_string($placeholderConfig) ? json_decode($placeholderConfig, true) : $placeholderConfig;
-
- foreach ($fillValues as $placeholder => $value) {
- $config = $placeholderConfig[$placeholder] ?? [];
- $fieldType = $config['field_type'] ?? 'text';
- $dataType = $config['data_type'] ?? 'user_input';
-
- Log::info('处理占位符', [
- 'placeholder' => $placeholder,
- 'value' => $value,
- 'field_type' => $fieldType,
- 'data_type' => $dataType
- ]);
-
- try {
- // 根据字段类型选择处理方式
- if ($fieldType === 'image' || $dataType === 'sign_img' || $dataType === 'signature') {
- // 处理图片类型
- $this->setImageValue($templateProcessor, $placeholder, $value);
- } else {
- // 处理文本类型
- $templateProcessor->setValue($placeholder, $value);
- }
- } catch (\Exception $e) {
- Log::error('占位符处理失败', [
- 'placeholder' => $placeholder,
- 'value' => $value,
- 'error' => $e->getMessage()
- ]);
- // 如果图片处理失败,尝试作为文本处理
- $templateProcessor->setValue($placeholder, $value);
- }
- }
- }
-
- /**
- * 设置图片占位符的值
- * @param TemplateProcessor $templateProcessor
- * @param string $placeholder
- * @param mixed $value
- * @return void
- */
- private function setImageValue(TemplateProcessor $templateProcessor, string $placeholder, $value)
- {
- if (empty($value)) {
- // 如果值为空,设置为空文本
- $templateProcessor->setValue($placeholder, '');
- return;
- }
-
- $imagePath = null;
-
- try {
- // 判断图片数据类型并处理
- if (is_string($value)) {
- if (str_starts_with($value, 'data:image/')) {
- // 处理base64图片数据
- $imagePath = $this->saveBase64Image($value);
- } elseif (str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) {
- // 处理网络图片URL
- $imagePath = $this->downloadImage($value);
- } elseif (file_exists($value)) {
- // 处理本地文件路径
- $imagePath = $value;
- } elseif (file_exists(public_path() . '/' . ltrim($value, '/'))) {
- // 处理相对路径
- $imagePath = public_path() . '/' . ltrim($value, '/');
- }
- }
-
- if ($imagePath && file_exists($imagePath)) {
- // 验证图片文件
- $imageInfo = getimagesize($imagePath);
- if ($imageInfo === false) {
- throw new \Exception('无效的图片文件');
- }
-
- // 设置图片,限制尺寸
- $templateProcessor->setImageValue($placeholder, [
- 'path' => $imagePath,
- 'width' => 100, // 可以根据需要调整
- 'height' => 100,
- 'ratio' => true
- ]);
-
- Log::info('图片占位符设置成功', [
- 'placeholder' => $placeholder,
- 'image_path' => $imagePath,
- 'image_size' => $imageInfo
- ]);
-
- // 如果是临时文件,标记稍后删除
- if (str_contains($imagePath, sys_get_temp_dir())) {
- register_shutdown_function(function() use ($imagePath) {
- if (file_exists($imagePath)) {
- @unlink($imagePath);
- }
- });
- }
- } else {
- // 如果无法处理为图片,使用文本替换
- $templateProcessor->setValue($placeholder, $value);
- Log::warning('图片处理失败,使用文本替换', [
- 'placeholder' => $placeholder,
- 'value' => $value
- ]);
- }
- } catch (\Exception $e) {
- Log::error('图片设置失败', [
- 'placeholder' => $placeholder,
- 'value' => $value,
- 'error' => $e->getMessage()
- ]);
- // 如果图片处理失败,使用文本替换
- $templateProcessor->setValue($placeholder, $value);
- }
- }
-
- /**
- * 保存base64图片数据到临时文件
- * @param string $base64Data
- * @return string|null
- */
- private function saveBase64Image(string $base64Data): ?string
- {
- try {
- // 解析base64数据
- if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $base64Data, $matches)) {
- $imageType = $matches[1];
- $imageData = base64_decode($matches[2]);
-
- if ($imageData === false) {
- throw new \Exception('base64解码失败');
- }
-
- // 创建临时文件
- $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images';
- if (!is_dir($tempDir)) {
- mkdir($tempDir, 0755, true);
- }
-
- $fileName = uniqid('img_') . '.' . $imageType;
- $filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName;
-
- if (file_put_contents($filePath, $imageData) === false) {
- throw new \Exception('保存图片文件失败');
- }
-
- return $filePath;
- }
- } catch (\Exception $e) {
- Log::error('保存base64图片失败:' . $e->getMessage());
- }
-
- return null;
- }
-
- /**
- * 下载网络图片到临时文件
- * @param string $url
- * @return string|null
- */
- private function downloadImage(string $url): ?string
- {
- try {
- // 创建临时文件
- $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'niucloud_images';
- if (!is_dir($tempDir)) {
- mkdir($tempDir, 0755, true);
- }
-
- $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
- $extension = $pathInfo['extension'] ?? 'jpg';
- $fileName = uniqid('img_') . '.' . $extension;
- $filePath = $tempDir . DIRECTORY_SEPARATOR . $fileName;
-
- // 下载图片
- $context = stream_context_create([
- 'http' => [
- 'timeout' => 30,
- 'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)',
- ]
- ]);
-
- $imageData = file_get_contents($url, false, $context);
- if ($imageData === false) {
- throw new \Exception('下载图片失败');
- }
-
- if (file_put_contents($filePath, $imageData) === false) {
- throw new \Exception('保存图片文件失败');
- }
-
- // 验证是否为有效图片
- if (getimagesize($filePath) === false) {
- unlink($filePath);
- throw new \Exception('下载的文件不是有效图片');
- }
-
- return $filePath;
- } catch (\Exception $e) {
- Log::error('下载图片失败:' . $e->getMessage(), ['url' => $url]);
- }
-
- return null;
- }
-
- /**
- * 处理Word文档中的URL图片,将URL转换为嵌入的图片附件
- * @param string $documentPath Word文档路径
- * @param array $fillValues 填充值数组(用于识别哪些是图片URL)
- * @return bool
- */
- private function processUrlImagesInDocument(string $documentPath, array $fillValues): bool
- {
- try {
- Log::info('开始处理Word文档中的URL图片', [
- 'document_path' => $documentPath,
- 'fill_values_count' => count($fillValues)
- ]);
-
- // 创建临时工作目录
- $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_url_processing_' . uniqid();
- if (!mkdir($tempDir, 0755, true)) {
- throw new \Exception('无法创建临时工作目录');
- }
-
- try {
- // 1. 解压Word文档到临时目录
- $zip = new \ZipArchive();
- $result = $zip->open($documentPath);
-
- if ($result !== TRUE) {
- throw new \Exception('无法打开Word文档,错误代码:' . $result);
- }
-
- if (!$zip->extractTo($tempDir)) {
- $zip->close();
- throw new \Exception('无法解压Word文档');
- }
- $zip->close();
-
- // 2. 读取document.xml文件
- $documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml';
- if (!file_exists($documentXmlPath)) {
- throw new \Exception('Word文档缺少document.xml文件');
- }
-
- $xmlContent = file_get_contents($documentXmlPath);
- if ($xmlContent === false) {
- throw new \Exception('无法读取document.xml文件内容');
- }
-
- // 3. 查找并处理URL图片
- $urlImageMap = $this->findUrlImagesInFillValues($fillValues);
-
- if (empty($urlImageMap)) {
- Log::info('未找到需要处理的URL图片');
- return true;
- }
-
- // 4. 下载图片并创建关系文件
- $imageRelations = $this->downloadAndCreateImageRelations($urlImageMap, $tempDir);
-
- if (empty($imageRelations)) {
- Log::warning('没有成功下载任何图片');
- return true;
- }
-
- // 5. 更新document.xml中的URL为图片引用
- $modifiedXmlContent = $this->replaceUrlsWithImageReferences($xmlContent, $imageRelations);
-
- // 6. 更新关系文件
- $this->updateDocumentRelations($tempDir, $imageRelations);
-
- // 7. 写回修改后的document.xml
- if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) {
- throw new \Exception('无法写入修改后的document.xml');
- }
-
- // 8. 重新打包Word文档
- $newZip = new \ZipArchive();
- $result = $newZip->open($documentPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
-
- if ($result !== TRUE) {
- throw new \Exception('无法重新打开Word文件,错误代码:' . $result);
- }
-
- // 递归添加所有文件到ZIP
- $this->addDirectoryToZip($newZip, $tempDir, '');
-
- if (!$newZip->close()) {
- throw new \Exception('无法保存修改后的Word文件');
- }
-
- Log::info('URL图片处理完成', [
- 'processed_images' => count($imageRelations),
- 'document_size' => filesize($documentPath)
- ]);
-
- return true;
-
- } finally {
- // 清理临时目录
- $this->removeDirectory($tempDir);
- }
-
- } catch (\Exception $e) {
- Log::error('处理Word文档URL图片失败', [
- 'document_path' => $documentPath,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- // 即使失败也不影响主流程
- return false;
- }
- }
-
- /**
- * 从填充值中查找URL图片
- * @param array $fillValues
- * @return array
- */
- private function findUrlImagesInFillValues(array $fillValues): array
- {
- $urlImages = [];
-
- foreach ($fillValues as $placeholder => $value) {
- if (is_string($value)) {
- // 检查是否为HTTP/HTTPS URL
- if (preg_match('/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i', $value)) {
- $urlImages[$placeholder] = $value;
- Log::info('发现URL图片', [
- 'placeholder' => $placeholder,
- 'url' => $value
- ]);
- }
- }
- }
-
- return $urlImages;
- }
-
- /**
- * 下载图片并创建图片关系
- * @param array $urlImageMap
- * @param string $tempDir
- * @return array
- */
- private function downloadAndCreateImageRelations(array $urlImageMap, string $tempDir): array
- {
- $imageRelations = [];
- $relationId = 1000; // 起始关系ID,避免与现有关系冲突
-
- // 确保media目录存在
- $mediaDir = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'media';
- if (!is_dir($mediaDir)) {
- mkdir($mediaDir, 0755, true);
- }
-
- foreach ($urlImageMap as $placeholder => $url) {
- try {
- // 下载图片
- $imageData = $this->downloadImageData($url);
- if (!$imageData) {
- continue;
- }
-
- // 确定文件扩展名
- $pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
- $extension = strtolower($pathInfo['extension'] ?? 'jpg');
-
- // 验证图片数据
- $tempImagePath = tempnam(sys_get_temp_dir(), 'img_validate_');
- file_put_contents($tempImagePath, $imageData);
- $imageInfo = getimagesize($tempImagePath);
- unlink($tempImagePath);
-
- if ($imageInfo === false) {
- Log::warning('下载的文件不是有效图片', ['url' => $url]);
- continue;
- }
-
- // 生成文件名和关系ID
- $fileName = 'image' . $relationId . '.' . $extension;
- $filePath = $mediaDir . DIRECTORY_SEPARATOR . $fileName;
- $relationIdStr = 'rId' . $relationId;
-
- // 保存图片文件
- if (file_put_contents($filePath, $imageData) === false) {
- Log::error('保存图片文件失败', ['file_path' => $filePath]);
- continue;
- }
-
- // 记录图片关系信息
- $imageRelations[$placeholder] = [
- 'url' => $url,
- 'relation_id' => $relationIdStr,
- 'file_name' => $fileName,
- 'file_path' => $filePath,
- 'width' => $imageInfo[0],
- 'height' => $imageInfo[1],
- 'mime_type' => $imageInfo['mime']
- ];
-
- $relationId++;
-
- Log::info('图片下载成功', [
- 'placeholder' => $placeholder,
- 'url' => $url,
- 'file_name' => $fileName,
- 'size' => $imageInfo[0] . 'x' . $imageInfo[1]
- ]);
-
- } catch (\Exception $e) {
- Log::error('处理图片失败', [
- 'placeholder' => $placeholder,
- 'url' => $url,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- return $imageRelations;
- }
-
- /**
- * 下载图片数据
- * @param string $url
- * @return string|false
- */
- private function downloadImageData(string $url)
- {
- try {
- $context = stream_context_create([
- 'http' => [
- 'timeout' => 30,
- 'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)',
- 'follow_location' => true,
- 'max_redirects' => 3
- ]
- ]);
-
- return file_get_contents($url, false, $context);
- } catch (\Exception $e) {
- Log::error('下载图片数据失败:' . $e->getMessage(), ['url' => $url]);
- return false;
- }
- }
-
- /**
- * 将XML中的URL替换为图片引用
- * @param string $xmlContent
- * @param array $imageRelations
- * @return string
- */
- private function replaceUrlsWithImageReferences(string $xmlContent, array $imageRelations): string
- {
- $modifiedContent = $xmlContent;
-
- foreach ($imageRelations as $placeholder => $relation) {
- $url = $relation['url'];
-
- // 构建图片引用XML
- $imageXml = $this->generateImageXml($relation);
-
- // 替换URL为图片引用
- $modifiedContent = str_replace($url, $imageXml, $modifiedContent);
-
- Log::info('URL替换为图片引用', [
- 'placeholder' => $placeholder,
- 'url' => $url,
- 'relation_id' => $relation['relation_id']
- ]);
- }
-
- return $modifiedContent;
- }
-
- /**
- * 生成图片引用XML
- * @param array $relation
- * @return string
- */
- private function generateImageXml(array $relation): string
- {
- // 强制限制最大尺寸
- $maxWidth = 40; // 最大宽度(像素)
- $maxHeight = 30; // 最大高度(像素)
-
- $originalWidth = $relation['width'];
- $originalHeight = $relation['height'];
-
- Log::info('图片原始尺寸', [
- 'original_width' => $originalWidth,
- 'original_height' => $originalHeight,
- 'max_width' => $maxWidth,
- 'max_height' => $maxHeight
- ]);
-
- // 计算缩放比例 - 如果图片超过最大尺寸就缩放,否则使用较小的固定尺寸
- $displayWidth = $maxWidth;
- $displayHeight = $maxHeight;
-
- if ($originalWidth > 0 && $originalHeight > 0) {
- // 计算保持宽高比的缩放
- $widthRatio = $maxWidth / $originalWidth;
- $heightRatio = $maxHeight / $originalHeight;
- $ratio = min($widthRatio, $heightRatio); // 移除1的限制,允许缩放
-
- $displayWidth = (int)($originalWidth * $ratio);
- $displayHeight = (int)($originalHeight * $ratio);
-
- // 确保不超过最大尺寸
- $displayWidth = min($displayWidth, $maxWidth);
- $displayHeight = min($displayHeight, $maxHeight);
- }
-
- Log::info('图片显示尺寸', [
- 'display_width' => $displayWidth,
- 'display_height' => $displayHeight,
- 'ratio' => isset($ratio) ? $ratio : 'fixed'
- ]);
-
- // Word中的尺寸单位转换(像素转EMU)
- $emuWidth = $displayWidth * 9525; // 1像素 = 9525 EMU
- $emuHeight = $displayHeight * 9525;
-
- $relationId = $relation['relation_id'];
-
- return '
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-';
- }
-
- /**
- * 更新文档关系文件
- * @param string $tempDir
- * @param array $imageRelations
- * @return void
- */
- private function updateDocumentRelations(string $tempDir, array $imageRelations): void
- {
- try {
- $relsPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . '_rels' . DIRECTORY_SEPARATOR . 'document.xml.rels';
-
- // 如果关系文件不存在,创建基础关系文件
- if (!file_exists($relsPath)) {
- $relsDir = dirname($relsPath);
- if (!is_dir($relsDir)) {
- mkdir($relsDir, 0755, true);
- }
-
- $baseRelsContent = '
-
-';
- file_put_contents($relsPath, $baseRelsContent);
- }
-
- // 读取现有关系文件
- $relsContent = file_get_contents($relsPath);
- if ($relsContent === false) {
- throw new \Exception('无法读取关系文件');
- }
-
- // 解析XML
- $dom = new \DOMDocument('1.0', 'UTF-8');
- $dom->preserveWhiteSpace = false;
- $dom->formatOutput = true;
-
- if (!$dom->loadXML($relsContent)) {
- throw new \Exception('无法解析关系文件XML');
- }
-
- $relationshipsElement = $dom->documentElement;
-
- // 添加图片关系
- foreach ($imageRelations as $relation) {
- $relationshipElement = $dom->createElement('Relationship');
- $relationshipElement->setAttribute('Id', $relation['relation_id']);
- $relationshipElement->setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
- $relationshipElement->setAttribute('Target', 'media/' . $relation['file_name']);
-
- $relationshipsElement->appendChild($relationshipElement);
- }
-
- // 写回关系文件
- if (file_put_contents($relsPath, $dom->saveXML()) === false) {
- throw new \Exception('无法保存关系文件');
- }
-
- Log::info('文档关系文件更新成功', [
- 'relations_added' => count($imageRelations),
- 'rels_path' => $relsPath
- ]);
-
- } catch (\Exception $e) {
- Log::error('更新文档关系文件失败', [
- 'error' => $e->getMessage(),
- 'temp_dir' => $tempDir
- ]);
- throw $e;
- }
- }
}
\ No newline at end of file
diff --git a/niucloud/app/service/admin/upgrade/UpgradeService.php b/niucloud/app/service/admin/upgrade/UpgradeService.php
index 2aa5b069..abe9a14d 100755
--- a/niucloud/app/service/admin/upgrade/UpgradeService.php
+++ b/niucloud/app/service/admin/upgrade/UpgradeService.php
@@ -317,14 +317,14 @@ class UpgradeService extends BaseAdminService
if ($addon == AddonDict::FRAMEWORK_KEY) {
$composer = '/niucloud/composer.json';
- $admin_package = '/admin/mysql-mcp-config.json';
- $web_package = '/web/mysql-mcp-config.json';
- $uniapp_package = '/uni-app/mysql-mcp-config.json';
+ $admin_package = '/admin/package.json';
+ $web_package = '/web/package.json';
+ $uniapp_package = '/uniapp/package.json';
} else {
$composer = "/niucloud/addon/{$addon}/package/composer.json";
- $admin_package = "/niucloud/addon/{$addon}/package/admin-mysql-mcp-config.json";
- $web_package = "/niucloud/addon/{$addon}/package/web-mysql-mcp-config.json";
- $uniapp_package = "/niucloud/addon/{$addon}/package/uni-app-mysql-mcp-config.json";
+ $admin_package = "/niucloud/addon/{$addon}/package/admin-package.json";
+ $web_package = "/niucloud/addon/{$addon}/package/web-package.json";
+ $uniapp_package = "/niucloud/addon/{$addon}/package/uni-app-package.json";
}
if (in_array($composer, $change_files)) {
@@ -392,7 +392,7 @@ class UpgradeService extends BaseAdminService
$this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon);
// 合并插件依赖
- $addon_uniapp_package = str_replace('/', DIRECTORY_SEPARATOR, project_path() . "niucloud/addon/{$addon}/package/uni-app-mysql-mcp-config.json");
+ $addon_uniapp_package = str_replace('/', DIRECTORY_SEPARATOR, project_path() . "niucloud/addon/{$addon}/package/uni-app-package.json");
if (file_exists($addon_uniapp_package)) {
$original = $depend_service->getNpmContent('uni-app');
@@ -402,7 +402,7 @@ class UpgradeService extends BaseAdminService
$original[$name] = isset($original[$name]) && is_array($original[$name]) ? array_merge($original[$name], $new[$name]) : $new[$name];
}
- $uniapp_package = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json';
+ $uniapp_package = $this->root_path . 'uniapp' . DIRECTORY_SEPARATOR . 'package.json';
$depend_service->writeArrayToJsonFile($original, $uniapp_package);
}
}
diff --git a/niucloud/app/service/core/addon/CoreAddonInstallService.php b/niucloud/app/service/core/addon/CoreAddonInstallService.php
index 7d171091..9b9dca1d 100755
--- a/niucloud/app/service/core/addon/CoreAddonInstallService.php
+++ b/niucloud/app/service/core/addon/CoreAddonInstallService.php
@@ -220,10 +220,10 @@ class CoreAddonInstallService extends CoreAddonBaseService
}, $package_file);
$tips = [get_lang('dict_addon.install_after_update')];
- if (in_array('admin-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_admin_update');
+ if (in_array('admin-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_admin_update');
if (in_array('composer.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_composer_update');
- if (in_array('uni-app-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_wap_update');
- if (in_array('web-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_web_update');
+ if (in_array('uni-app-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_wap_update');
+ if (in_array('web-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_web_update');
return $tips;
}
return true;
diff --git a/niucloud/app/service/core/addon/CoreDependService.php b/niucloud/app/service/core/addon/CoreDependService.php
index a23ac046..3d5d8db6 100755
--- a/niucloud/app/service/core/addon/CoreDependService.php
+++ b/niucloud/app/service/core/addon/CoreDependService.php
@@ -25,9 +25,9 @@ class CoreDependService extends CoreAddonBaseService
{
parent::__construct();
$this->server_composer_file = $this->root_path . 'niucloud' . DIRECTORY_SEPARATOR . 'composer.json';
- $this->admin_npm_file = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json';
- $this->web_npm_file = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json';
- $this->wap_npm_file = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json';
+ $this->admin_npm_file = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'package.json';
+ $this->web_npm_file = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'package.json';
+ $this->wap_npm_file = $this->root_path . 'uniapp' . DIRECTORY_SEPARATOR . 'package.json';
}
/**
@@ -141,11 +141,11 @@ class CoreDependService extends CoreAddonBaseService
public function getAddonNpmContent(string $addon, string $type)
{
if ($type == 'admin') {
- $file_path = $this->geAddonPackagePath($addon) . 'admin-mysql-mcp-config.json';
+ $file_path = $this->geAddonPackagePath($addon) . 'admin-package.json';
} elseif ($type == 'web') {
- $file_path = $this->geAddonPackagePath($addon) . 'web-mysql-mcp-config.json';
+ $file_path = $this->geAddonPackagePath($addon) . 'web-package.json';
} else {
- $file_path = $this->geAddonPackagePath($addon) . 'uni-app-mysql-mcp-config.json';
+ $file_path = $this->geAddonPackagePath($addon) . 'uni-app-package.json';
}
return $this->jsonFileToArray($file_path);
}
diff --git a/niucloud/bug.md b/niucloud/bug.md
new file mode 100644
index 00000000..e69de29b
diff --git a/question.md b/question.md
new file mode 100644
index 00000000..773e157e
--- /dev/null
+++ b/question.md
@@ -0,0 +1,84 @@
+# 智慧教务系统 - 问题记录
+
+> 记录浏览代码过程中遇到的业务问题,等待老王解答
+
+## 1. 课程预约流程
+
+**问题**:学员预约课程的完整流程是什么?
+
+- 学员如何查看可预约的课程?
+- 预约是否需要教练确认?
+- 预约失败的原因有哪些?
+
+**涉及文件**:
+- `niucloud/app/api/controller/` (预约相关控制器)
+- `uniapp/pages/student/` (学员端页面)
+
+---
+
+## 2. 消课机制
+
+**问题**:消课是如何触发的?
+
+- 教练手动消课还是系统自动消课?
+- 消课前是否需要学员确认?
+- 消课金额如何计算?
+
+**涉及文件**:
+- `niucloud/app/service/api/` (消课相关 Service)
+- `uniapp/pages/` (消课页面)
+
+---
+
+## 3. 学员卡类型
+
+**问题**:学员卡的类型和有效期规则是什么?
+
+- 看到学员卡相关的数据库表
+- 支持哪些类型的卡(次卡、月卡、季卡等)?
+- 过期后如何处理?
+
+---
+
+## 4. 教练排班
+
+**问题**:教练的排班是如何管理的?
+
+- 排班是教练自己设置还是管理员设置?
+- 排班冲突如何检测?
+- 临时调课如何处理?
+
+---
+
+## 5. 多端权限
+
+**问题**:学员端和教练端的权限如何区分?
+
+- 看到 `pages/student/` 和 `pages/coach/` 目录
+- 权限是如何控制的?
+- 是否支持管理员角色?
+
+---
+
+## 6. 数据同步
+
+**问题**:管理后台和移动端的数据如何同步?
+
+- 看到有 `adminapi` 和 `api` 两套接口
+- 数据是否实时同步?
+- 是否有离线支持?
+
+---
+
+## 待确认的业务规则
+
+1. **预约规则**:学员可以提前多久预约?是否可以取消预约?
+2. **消课规则**:消课后是否立即扣减学员卡余额?
+3. **教练提成**:教练的提成是如何计算的?
+4. **退款规则**:学员如何申请退款?退款流程是什么?
+5. **课时管理**:总课时和已消课时的关系是什么?
+
+---
+
+> **记录时间**:2026-02-01
+> **状态**:待回答
diff --git a/uniapp/bug.md b/uniapp/bug.md
new file mode 100644
index 00000000..e69de29b