Procházet zdrojové kódy

Merge remote-tracking branch 'origin/cfclub' into cfclub

liaoxg před 1 dnem
rodič
revize
0210676e0b

+ 7 - 25
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -899,10 +899,9 @@ public class HealthReportController {
         String originalFilename = draft.getOriginalFilename();
 
         try {
-            // LangGraph 优先:远程 OSS URL 直接传 URL(LangGraph 自行下载),本地路径传共享卷路径
+            // LangGraph 需要本地路径:经 getFilePath 下载到共享卷(LangGraph 容器可读)
             try {
-                String lgPath = (fileUrl != null && fileUrl.startsWith("http"))
-                        ? fileUrl : storageService.getFilePath(fileUrl);
+                String lgPath = storageService.getFilePath(fileUrl);
                 Map<String, Object> lgRequest = new HashMap<>();
                 lgRequest.put("file_path", lgPath);
                 lgRequest.put("user_id", userId);
@@ -1081,12 +1080,11 @@ public class HealthReportController {
 
             // 保存文件(通过 StorageService 支持本地磁盘或京东云 OSS)
             String fileUrl = storageService.storeFile(file, "health-reports");
-            boolean isRemote = fileUrl != null && fileUrl.startsWith("http");
 
-            // 尝试 LangGraph 远程解析(远程 OSS URL 直接传 URL,LangGraph 自行下载)
+            // 尝试 LangGraph 解析(LangGraph 只认本地路径,必须经 getFilePath 下载到共享卷)
             try {
                 Map<String, Object> lgRequest = new HashMap<>();
-                lgRequest.put("file_path", isRemote ? fileUrl : storageService.getFilePath(fileUrl));
+                lgRequest.put("file_path", storageService.getFilePath(fileUrl));
                 lgRequest.put("user_id", userId);
                 if (familyId != null) {
                     lgRequest.put("family_id", familyId);
@@ -2439,7 +2437,8 @@ public class HealthReportController {
         }
 
         try {
-            String absolutePath = saveUploadFileForProxy(file, userId);
+            String fileUrl = storageService.storeFile(file, "health-reports");
+            String absolutePath = storageService.getFilePath(fileUrl);
 
             // 调用 LangGraph 解析
             Map<String, Object> result = callLangGraphParse(absolutePath, userId, familyId, subjectId);
@@ -2448,6 +2447,7 @@ public class HealthReportController {
                 return Result.error("报告解析失败");
             }
 
+            result.put("fileUrl", fileUrl);
             // 保存草稿
             Long draftId = (Long) result.get("draftId");
             if (draftId != null && note != null) {
@@ -2519,24 +2519,6 @@ public class HealthReportController {
         }
     }
 
-    /**
-     * 代上传场景:保存文件(用代上传人目录)
-     */
-    private String saveUploadFileForProxy(MultipartFile file, Long proxyUserId) throws IOException {
-        String uploadDir = "uploads" + File.separator + "health-reports-proxy" + File.separator + proxyUserId;
-        File dir = new File(uploadDir);
-        if (!dir.exists()) {
-            dir.mkdirs();
-        }
-        String originalFilename = file.getOriginalFilename();
-        if (originalFilename == null) originalFilename = "report.pdf";
-        String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));
-        String savedFilename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + extension;
-        Path filePath = Paths.get(uploadDir, savedFilename);
-        Files.copy(file.getInputStream(), filePath);
-        return uploadDir.replace("\\", "/") + "/" + savedFilename;
-    }
-
     /**
      * 调用 LangGraph 解析报告
      */

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -114,6 +114,9 @@ public class AdminArticleController {
         if (body.get("difficultyLevel") != null && !body.get("difficultyLevel").toString().trim().isEmpty()) {
             article.setDifficultyLevel(Integer.valueOf(body.get("difficultyLevel").toString()));
         }
+        if (body.get("sortOrder") != null && !body.get("sortOrder").toString().trim().isEmpty()) {
+            article.setSortOrder(Integer.valueOf(body.get("sortOrder").toString()));
+        }
         // relatedDimensions may come as array from frontend
         Object rd = body.get("relatedDimensions");
         if (rd instanceof List) {
@@ -195,6 +198,9 @@ public class AdminArticleController {
         if (body.get("difficultyLevel") != null && !body.get("difficultyLevel").toString().trim().isEmpty()) {
             article.setDifficultyLevel(Integer.valueOf(body.get("difficultyLevel").toString()));
         }
+        if (body.get("sortOrder") != null && !body.get("sortOrder").toString().trim().isEmpty()) {
+            article.setSortOrder(Integer.valueOf(body.get("sortOrder").toString()));
+        }
         Object rd = body.get("relatedDimensions");
         if (rd instanceof List) {
             article.setRelatedDimensions(String.join(",", (List<String>) rd));

+ 28 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/impl/JdCloudStorageService.java

@@ -25,9 +25,11 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.time.LocalDate;
@@ -61,6 +63,13 @@ public class JdCloudStorageService implements StorageService {
     @Value("${jdcloud.region:cn-north-1}")
     private String region;
 
+    /**
+     * 本地共享目录。京东云对象在供本地进程(如 cfc-langgraph 容器)按路径读取时,
+     * 会被下载到该目录下的缓存子目录,确保 LangGraph 能读到文件。
+     */
+    @Value("${upload.base-dir:/data/cfc-uploads}")
+    private String uploadBaseDir;
+
     @Resource
     private FileRecordMapper fileRecordMapper;
 
@@ -153,25 +162,35 @@ public class JdCloudStorageService implements StorageService {
 
     @Override
     public String getFilePath(String filename) {
-        // 京东云文件不在本地磁盘,下载到临时目录供本地解析使用
+        // 京东云文件不在本地磁盘:下载到共享卷 upload.base-dir 下的缓存目录,
+        // 供本地进程(含 cfc-langgraph 容器)按本地路径读取;缓存目录需可被其它用户遍历/读取。
         try {
             String key = extractKey(filename);
-            S3Object object = s3Client.getObject(bucket, key);
-
-            Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "cfc-jdcloud");
-            Files.createDirectories(tempDir);
-            String localName = key.substring(key.lastIndexOf('/') + 1);
-            Path tempFile = tempDir.resolve(UUID.randomUUID().toString() + "-" + localName);
+            Path cacheDir = Paths.get(uploadBaseDir, ".jdcloud-cache").toAbsolutePath().normalize();
+            Files.createDirectories(cacheDir);
+            cacheDir.toFile().setReadable(true, false);
+            cacheDir.toFile().setExecutable(true, false);
+
+            String localName = sha256(key.getBytes(StandardCharsets.UTF_8))
+                    + "-" + key.substring(key.lastIndexOf('/') + 1);
+            Path cached = cacheDir.resolve(localName);
+            if (Files.exists(cached) && Files.size(cached) > 0) {
+                return cached.toString();
+            }
 
+            S3Object object = s3Client.getObject(bucket, key);
+            Path tmp = cacheDir.resolve(localName + ".part");
             try (InputStream in = object.getObjectContent();
-                 FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
+                 FileOutputStream out = new FileOutputStream(tmp.toFile())) {
                 byte[] buffer = new byte[8192];
                 int len;
                 while ((len = in.read(buffer)) != -1) {
                     out.write(buffer, 0, len);
                 }
             }
-            return tempFile.toString();
+            Files.move(tmp, cached, StandardCopyOption.REPLACE_EXISTING);
+            cached.toFile().setReadable(true, false);
+            return cached.toString();
         } catch (IOException e) {
             throw new RuntimeException("下载京东云文件失败: " + filename, e);
         }

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-6163c4cf76c3482f30a9b2e670e7147243667a32
+89adf93c555227a41ef8c0fa5ed6811aa3845647

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1583",
+  "version": "1.0.1584",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1583",
+      "version": "1.0.1584",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 4 - 0
cfc-web/package.json

@@ -1,6 +1,10 @@
 {
   "name": "cfc-web",
+<<<<<<< HEAD
+  "version": "1.0.1585",
+=======
   "version": "1.0.1584",
+>>>>>>> b14532bae (chore: rebuild dist 2026-10-12 11:33)
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 25 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,31 @@
 
 ---
 
+<<<<<<< HEAD
+## v1.0.1585 (2026-10-12)
+
+### Bug 修复
+- 修复 product-edit 商品编辑页方法错位
+- 修复 product-edit/comparison 模板与 api.js 编译错误
+
+### 其他
+- 拆分为: onTypeChange 仅同步 picker 选择; loadProduct(id) 承载商品详情加载回填逻辑。
+- 
+- - pages/dan-assessment/comparison.vue: 修 v-if 引号与 v-else 配对; 改用 @/utils/api 命名导入(@/utils/request 不存在); onLoad 去重复 loadReportIds
+- - pages/vendor/product-edit/product-edit.vue: 补 form-section 缺失的 </view>
+- 
+
+### 新功能
+- 本地程序化生成形象动作帧(item 3)
+- provider-agnostic 动作帧序列 schema + 客户端播放(item 3 基础)
+- 微信网站应用扫码登录(桌面端 SSO)后端
+
+### 重构
+- 扫码登录成功回传 userId/role,移除多余 allow(dead_code)
+
+
+=======
+>>>>>>> b14532bae (chore: rebuild dist 2026-10-12 11:33)
 ## v1.0.1584 (2026-10-12)
 
 ### Bug 修复

+ 29 - 0
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,10 @@
 # 更新日志
 
+<<<<<<< HEAD
+> 当前版本: v1.0.1585
+=======
 > 当前版本: v1.0.1584
+>>>>>>> b14532bae (chore: rebuild dist 2026-10-12 11:33)
 
 ## 历史版本
 
@@ -8,6 +12,31 @@
 
 ---
 
+<<<<<<< HEAD
+## v1.0.1585 (2026-10-12)
+
+### Bug 修复
+- 修复 product-edit 商品编辑页方法错位
+- 修复 product-edit/comparison 模板与 api.js 编译错误
+
+### 其他
+- 拆分为: onTypeChange 仅同步 picker 选择; loadProduct(id) 承载商品详情加载回填逻辑。
+- 
+- - pages/dan-assessment/comparison.vue: 修 v-if 引号与 v-else 配对; 改用 @/utils/api 命名导入(@/utils/request 不存在); onLoad 去重复 loadReportIds
+- - pages/vendor/product-edit/product-edit.vue: 补 form-section 缺失的 </view>
+- 
+
+### 新功能
+- 本地程序化生成形象动作帧(item 3)
+- provider-agnostic 动作帧序列 schema + 客户端播放(item 3 基础)
+- 微信网站应用扫码登录(桌面端 SSO)后端
+
+### 重构
+- 扫码登录成功回传 userId/role,移除多余 allow(dead_code)
+
+
+=======
+>>>>>>> b14532bae (chore: rebuild dist 2026-10-12 11:33)
 ## v1.0.1584 (2026-10-12)
 
 ### Bug 修复

+ 1 - 8
cfc-web/src/views/admin/ActivityEdit.vue

@@ -608,20 +608,13 @@ activityId
   overflow-y: auto;
 }
 
-/* 内滚动容器:利用 el-card__body flex column 让表单区域独立滚动 */
-.activity-edit-form-wrap {
-  flex: 1;
-  min-height: 0;
-  overflow-y: auto;
-  padding-right: 8px;
-  margin-right: -8px;
-}
 
 /* 内滚动容器:利用 el-card__body flex column 让表单区域独立滚动 */
 .activity-edit-form-wrap {
   flex: 1;
   min-height: 0;
   overflow-y: auto;
+  overflow-x: hidden;
   padding-right: 8px;
   margin-right: -8px;
 }

+ 9 - 0
cfc-web/src/views/admin/ArticleEdit.vue

@@ -133,6 +133,10 @@
             <el-radio label="private">私密</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="推荐排序">
+          <el-input-number v-model="form.sortOrder" :min="0" :max="9999" :disabled="readonly" controls-position="right" />
+          <span class="form-hint">越小越靠前,仅精选文章生效(列表上移/下移会自动调整)</span>
+        </el-form-item>
       </el-form>
       </div>
     </el-card>
@@ -190,6 +194,7 @@ export default {
         tagIds: [],
         author: '',
         readTime: 3,
+        sortOrder: 0,
         relatedDimensions: [],
         dimensionWeights: [],
         visibility: 'public',
@@ -615,6 +620,7 @@ onError: (file, err, res) => {
             tagIds: d.tagIds || [],
             author: d.author || '',
             readTime: d.readTime || 3,
+            sortOrder: d.sortOrder || 0,
             relatedDimensions: d.relatedDimensions || [],
             dimensionWeights: [],
             visibility: d.visibility || 'public',
@@ -653,6 +659,7 @@ onError: (file, err, res) => {
             tagIds: [],
             author: d.author || '',
             readTime: d.readTime || 3,
+            sortOrder: 0,
             relatedDimensions: d.relatedDimensions || [],
             dimensionWeights: [],
             visibility: d.visibility || 'public',
@@ -711,6 +718,7 @@ onError: (file, err, res) => {
         tagIds: this.form.tagIds,
         author: this.form.author,
         readTime: this.form.readTime,
+        sortOrder: this.form.sortOrder,
         relatedDimensions: this.form.relatedDimensions,
         visibility: this.form.visibility,
         visibleTo: this.form.visibleTo
@@ -1025,6 +1033,7 @@ onError: (file, err, res) => {
   flex: 1;
   min-height: 0;
   overflow-y: auto;
+  overflow-x: hidden;
   padding-right: 8px;
   margin-right: -8px;
 }