From 7d56ff4bfed7eee9da7c2434514fa818049c3aea Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Sat, 23 May 2026 01:29:43 +0800 Subject: [PATCH] fix: add placeholder data and error handling for nursing info display - Pre-populate nursingInfoList with placeholder to prevent empty RecyclerView - Add try-catch around JSON parsing in initNursingInfo - Add toast on parse failure Co-Authored-By: Claude Opus 4.6 --- docs/护理资讯模块改造文档.md | 252 ++++++++++++++++++ docs/改进完成记录.md | 38 ++- .../main/find/entity/homeListBean.java | 9 + .../service/main/task/HomeFragment.java | 44 +-- .../service/main/task/ViewHolderFive.java | 3 + .../view/webview/XfiveWebActivity.java | 20 +- .../controller/system/SysUserController.java | 1 + 7 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 docs/护理资讯模块改造文档.md diff --git a/docs/护理资讯模块改造文档.md b/docs/护理资讯模块改造文档.md new file mode 100644 index 0000000..409ea7c --- /dev/null +++ b/docs/护理资讯模块改造文档.md @@ -0,0 +1,252 @@ +# 护理资讯模块改造文档 + +> 日期:2026-05-22 ~ 2026-05-23 +> 状态:已完成并部署 + +--- + +## 一、背景 + +Android 首页"护理资讯"模块最初完全硬编码: +- **Android 端**:4 条写死的数据,点击统一跳转 `privacy.html` +- **后端 API**:`getAppIndexInfo()` 返回 3 条写死 JSON,从未查数据库 +- **管理后台**:无任何资讯管理页面 + +改造目标:建立完整的 **后台管理 → 数据库 → API → Android 展示** 数据链路。 + +--- + +## 二、架构总览 + +``` +管理后台(8050) 数据库 Android App + nursing/index.vue │ HomeFragment + │ 新增/编辑/删除 │ │ initNursingInfo() + ▼ ▼ ▼ + RlzNursingArticleController rlz_nursing_article GET /system/user/getAppIndexInfo + /system/nursing/* │ │ + │ │ ▼ + ▼ │ ViewHolderFive + IRlzNursingArticleService │ 动态渲染 RecyclerView + │ │ │ + ▼ │ XfiveWebActivity + RlzNursingArticleMapper │ 文章详情(WebView) +``` + +--- + +## 三、数据库设计 + +### rlz_nursing_article 表结构 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | BIGINT(20) | 主键自增 | +| title | VARCHAR(200) | 标题 | +| image_url | VARCHAR(500) | 封面图片 URL | +| article_url | VARCHAR(500) | 文章链接 URL(可选,有正文时可省略)| +| content | LONGTEXT | 文章正文(HTML 富文本)| +| sort_order | INT(4) | 排序号(越小越靠前)| +| status | CHAR(1) | 0=发布 1=隐藏 | +| view_count | BIGINT | 阅读次数 | +| create_by / create_time | - | 创建者/时间 | +| update_by / update_time | - | 更新者/时间 | +| remark | VARCHAR(500) | 备注 | + +### 菜单权限 + +在 `sys_menu` 中插入: +- **菜单**:系统管理 → 护理资讯(`system:nursing:list`) +- **按钮权限**:查询 / 新增 / 修改 / 删除 / 导出(`system:nursing:query/add/edit/remove/export`) + +--- + +## 四、后端 API + +### 管理后台接口(需认证) + +| 方法 | 路径 | 权限 | 说明 | +|------|------|------|------| +| GET | `/system/nursing/list` | `system:nursing:list` | 分页列表(标题模糊搜索 + 状态筛选)| +| GET | `/system/nursing/{id}` | `system:nursing:query` | 获取详情 | +| POST | `/system/nursing` | `system:nursing:add` | 新增 | +| PUT | `/system/nursing` | `system:nursing:edit` | 修改 | +| DELETE | `/system/nursing/{ids}` | `system:nursing:remove` | 批量删除 | +| POST | `/system/nursing/export` | `system:nursing:export` | Excel 导出 | + +### App 公开接口(无需认证) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/system/nursing/published` | 获取所有已发布文章(status=0,按 sort_order 排序)| +| PUT | `/system/nursing/view/{id}` | 阅读量 +1 | + +### 兼容接口 + +`GET /system/user/getAppIndexInfo` — 首页综合接口,`realTimeInfoList` 字段仍从 `rlz_nursing_article` 表查询(保持 Android 兼容)。 + +--- + +## 五、管理后台功能 + +访问地址:http://101.43.95.130:8050/ → 系统管理 → 护理资讯 + +| 功能 | 说明 | +|------|------| +| 搜索 | 标题模糊搜索 + 状态筛选(发布/隐藏)| +| 列表 | 排序、标题、封面缩略图、文章链接、阅读量、状态标签、操作按钮 | +| 新增/编辑 | 弹窗表单:标题 + 封面图片上传 + 文章链接 + **富文本正文** + 排序 + 状态 | +| 导出 | Excel 导出全部数据 | +| 删除 | 支持单选和批量删除 | + +### 富文本编辑器 + +使用 Quill 编辑器(`` 全局组件),支持: +- 加粗、斜体、下划线、删除线 +- 标题 H1-H6 +- 有序/无序列表、缩进 +- 文字颜色、背景色 +- 链接、图片、视频插入 +- 图片通过 `/common/upload` 上传到 OSS + +### 图片上传 + +封面图片使用 `` 全局组件: +- 支持 JPG/PNG/JPEG +- 自动上传到 `/common/upload` +- 表格中自动显示缩略图 + +--- + +## 六、Android 集成 + +### 数据流 + +``` +HomeFragment.initNursingInfo() + → OkGo GET /system/user/getAppIndexInfo + → 解析 JSON: realTimeInfoList[] + → title → content (显示标题) + → realTimeInfoImage → pic (封面图) + → realTimeInfoUrl → url (文章链接) + → 设置到 robCustomerInfoBean.nursingInfoList + → ViewHolderFive 渲染 +``` + +### 关键文件 + +| 文件 | 说明 | +|------|------| +| `HomeFragment.java:145` | `initNursingInfo()` — API 调用 + JSON 解析 | +| `ViewHolderFive.java` | 嵌套 RecyclerView 渲染文章列表,adapter 复用 | +| `homeListBean.java` | 数据模型:pic / content / url | +| `robCustomerInfoBean.java` | `nursingInfoList` 字段持有文章列表 | +| `HttpConstants.java` | `URi_system_getAppIndexInfo` 接口地址 | + +### 容错处理 + +| 场景 | 处理方式 | +|------|----------| +| 网络失败 | Toast 提示 + 使用上次缓存数据(`CacheMode.REQUEST_FAILED_READ_CACHE`)| +| 未登录点击 | 跳转登录页 | +| URL 为空 | 降级到 `privacy.html` 本地页面 | +| 列表为空 | 显示空列表(不报错)| + +--- + +## 七、改造历程 + +### 第一阶段:动态化(2026-05-22) + +- 新建 `rlz_nursing_article` 表 + 菜单权限 SQL +- 新建后端 CRUD 全套(Entity / Mapper / Service / Controller) +- 新建管理后台页面 `nursing/index.vue` +- 修改 `SysUserController.getAppIndexInfo()`,从 DB 查询替换硬编码 +- Android 端新增 `initNursingInfo()` API 调用 +- 将 API 路径加入 SecurityConfig 匿名白名单 + +### 第二阶段:功能增强(2026-05-23) + +- 新增 `content` LONGTEXT 字段 + 富文本编辑器 +- 新增 `view_count` 字段 + 阅读量统计 API +- 封面图片改为上传组件(替代手动贴 URL) +- 表格缩略图改为 `image-preview` 组件 +- 新增 `/published` 和 `/view/{id}` 公开端点 +- Android 端增加缓存策略和错误提示 +- `ViewHolderFive` adapter 改为成员变量复用 +- nginx 添加 `charset utf-8` 解决中文乱码 + +--- + +## 八、部署注意事项 + +### 部署顺序 + +1. **数据库**:执行 `sql/nursing_article.sql`(首次)或 ALTER TABLE(升级) +2. **后端**:`docker compose build --no-cache backend && docker compose up -d backend` +3. **前端**:`npm run build:prod` → 上传 dist/ 到服务器 → 重启 `rlz-ui-server` + +### 常见问题 + +| 问题 | 现象 | 解决 | +|------|------|------| +| 中文乱码 | 管理后台显示方块/问号 | nginx 添加 `charset utf-8;` 并重启;清除浏览器缓存 | +| 403 Forbidden | 访问首页返回 403 | nginx 容器重启:`docker restart rlz-ui-server` | +| API 401 | App 接口需要登录 | 检查 SecurityConfig 白名单是否包含接口路径 | +| JS 文件 404 | 新功能页面空白 | 确认 dist 文件完整部署,index.html 引用正确 | + +### 构建命令参考 + +```bash +# 管理后台构建(Node 22 需要 openssl-legacy-provider) +cd rlz-ui && NODE_OPTIONS=--openssl-legacy-provider npm run build:prod + +# 后端构建 +cd rlz && docker compose build --no-cache backend && docker compose up -d backend + +# 部署 dist(避免权限问题用 Docker 容器操作) +docker run --rm -v /path/to/rlz-ui:/data -v /tmp:/hosttmp alpine:latest \ + sh -c "rm -rf /data/dist/* && tar xzf /hosttmp/dist.tar.gz -C /data/" +docker restart rlz-ui-server +``` + +--- + +## 九、文件变更清单 + +| 文件 | 变更类型 | 阶段 | +|------|----------|------| +| `sql/nursing_article.sql` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../domain/RlzNursingArticle.java` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../mapper/RlzNursingArticleMapper.java` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../mapper/RlzNursingArticleMapper.xml` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../service/IRlzNursingArticleService.java` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../service/impl/RlzNursingArticleServiceImpl.java` | 新增 + 修改 | 一/二 | +| `ruoyi-system/.../controller/RlzNursingArticleController.java` | 新增 + 修改 | 一/二 | +| `ruoyi-admin/.../controller/system/SysUserController.java` | 修改 | 一 | +| `ruoyi-framework/.../config/SecurityConfig.java` | 修改 | 一/二 | +| `rlz-ui/src/api/system/nursing.js` | 新增 | 一 | +| `rlz-ui/src/views/system/nursing/index.vue` | 新增 + 修改 | 一/二 | +| `peizhen/.../homeListBean.java` | 修改 | 一 | +| `peizhen/.../robCustomerInfoBean.java` | 修改 | 一 | +| `peizhen/.../HttpConstants.java` | 修改 | 一 | +| `peizhen/.../HomeFragment.java` | 修改 | 一/二 | +| `peizhen/.../ViewHolderFive.java` | 修改 | 一/二 | + +--- + +## 十、经验总结 + +1. **FastJSON 依赖不在 ruoyi-system 模块中** — 只有 ruoyi-admin 有。跨模块写 Controller 时不要引入 FastJSON,直接返回 List 交给 AjaxResult 序列化。 + +2. **MyBatis LONGTEXT 字段需 cast** — 参照 SysNotice 模式,SELECT 时使用 `cast(content as char) as content`,否则 MyBatis 可能当作 BLOB 返回 byte[]。 + +3. **Docker bind mount 在文件被整个目录替换后会失效** — 通过 `docker run --rm -v alpine` 删除并解压 dist 后,必须 `docker restart rlz-ui-server` 重新挂载。 + +4. **nginx 默认不声明 charset** — 中文 JS 文件虽 UTF-8 编码正确,但 nginx 不加 `charset utf-8;` 浏览器可能按 Latin-1 解析导致乱码。 + +5. **Vue 全局组件可直接使用** — `` / `` / `` 在 main.js 已注册,页面中直接写标签即可,无需 import。 + +6. **Windows git 推送 Gitea 偶现超时** — 可能需要更换 SSH 端口或使用 HTTP 协议。本项目中通过 paramiko SFTP 直传文件作为备用部署方案。 + +7. **OkGo 的 REQUEST_FAILED_READ_CACHE 模式** — 首次请求成功后会缓存响应,后续网络失败时自动读缓存,适合首页不太频繁变动的数据。 diff --git a/docs/改进完成记录.md b/docs/改进完成记录.md index 75ffc1a..2419262 100644 --- a/docs/改进完成记录.md +++ b/docs/改进完成记录.md @@ -1,6 +1,6 @@ # 开发环境改进完成记录 -> 日期:2026-05-14 +> 日期:2026-05-14(更新于 2026-05-23) --- @@ -74,6 +74,26 @@ - `order_view` 缺少 `userb_age`/`userb_nation`/`userc_age`/`userc_nation` 列导致订单 API 500 错误 - 通过 `CREATE OR REPLACE VIEW` 用 `NULL AS` 占位缺失列,两库均已修复 +### 11. 护理资讯动态化改造 (Issue #22) — 2026-05-22 + +- 新建 `rlz_nursing_article` 表(标题/封面图/文章链接/排序/状态) +- 新建后端 CRUD 全套:Entity / Mapper / Service / Controller(`/system/nursing/*`) +- 新建管理后台页面 `views/system/nursing/index.vue`(搜索/表格/弹窗表单/分页) +- 修改 `SysUserController.getAppIndexInfo()` 从 DB 查询替换硬编码数据 +- Android 端新增 `HomeFragment.initNursingInfo()` API 调用动态渲染 +- 新增 `homeListBean.url` 和 `robCustomerInfoBean.nursingInfoList` 字段 +- SecurityConfig 添加 `/system/user/getAppIndexInfo` 匿名白名单 + +### 12. 护理资讯功能增强 — 2026-05-23 + +- 新增 `content` LONGTEXT 字段 + Quill 富文本编辑器(文章可自建内容) +- 新增 `view_count` BIGINT 字段 + `/view/{id}` 阅读量统计 API +- 封面图片改为 `` 上传组件(替代手动贴 URL) +- 新增 `/system/nursing/published` 公开端点(返回 content + viewCount) +- Android 端增加 `CacheMode.REQUEST_FAILED_READ_CACHE` 缓存策略 + Toast 错误提示 +- ViewHolderFive adapter 改为成员变量复用,避免反复创建 +- nginx 添加 `charset utf-8;` 修复中文乱码 + --- ## 后续需要手动完成的 @@ -117,3 +137,19 @@ | `coupon/app.js` | 修改(引用 config,修复 domain) | | `docs/开发环境方案.md` | 新增 | | `docs/改进完成记录.md` | 新增 | +| `docs/护理资讯模块改造文档.md` | 新增 | +| `rlz-system/.../domain/RlzNursingArticle.java` | 新增 | +| `rlz-system/.../mapper/RlzNursingArticleMapper.java` | 新增 | +| `rlz-system/.../mapper/RlzNursingArticleMapper.xml` | 新增 | +| `rlz-system/.../service/IRlzNursingArticleService.java` | 新增 | +| `rlz-system/.../service/impl/RlzNursingArticleServiceImpl.java` | 新增 | +| `rlz-system/.../controller/RlzNursingArticleController.java` | 新增 | +| `rlz-admin/.../controller/system/SysUserController.java` | 修改 | +| `rlz-framework/.../config/SecurityConfig.java` | 修改 | +| `rlz-ui/src/api/system/nursing.js` | 新增 | +| `rlz-ui/src/views/system/nursing/index.vue` | 新增 | +| `peizhen/.../homeListBean.java` | 修改 | +| `peizhen/.../robCustomerInfoBean.java` | 修改 | +| `peizhen/.../HomeFragment.java` | 修改 | +| `peizhen/.../ViewHolderFive.java` | 修改 | +| `sql/nursing_article.sql` | 新增 | diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/find/entity/homeListBean.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/find/entity/homeListBean.java index 68ff4f3..f7256a5 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/main/find/entity/homeListBean.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/find/entity/homeListBean.java @@ -4,6 +4,7 @@ public class homeListBean { private String pic; private String content; private String url; + private String articleBody; public String getPic() { return pic; @@ -28,4 +29,12 @@ public class homeListBean { public void setUrl(String url) { this.url = url; } + + public String getArticleBody() { + return articleBody; + } + + public void setArticleBody(String articleBody) { + this.articleBody = articleBody; + } } diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/task/HomeFragment.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/task/HomeFragment.java index 81fab1a..4e049fe 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/main/task/HomeFragment.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/task/HomeFragment.java @@ -62,6 +62,14 @@ public class HomeFragment extends BaseFragment { mContentView = inflater.inflate(R.layout.fragment_home_layout, container, false); reply_rcey = (RecyclerView) mContentView.findViewById(R.id.marking_fragment_recyclerView); robCustomerInfoBean = new robCustomerInfoBean(); + // 先填充占位数据,避免 inner RecyclerView 高度为 0 导致护理资讯区域不显示 + List placeholder = new ArrayList<>(); + homeListBean pb = new homeListBean(); + pb.setContent("正在加载..."); + pb.setPic(""); + pb.setUrl(""); + placeholder.add(pb); + robCustomerInfoBean.setNursingInfoList(placeholder); initRecyclerView(robCustomerInfoBean); //创建一个过滤器对象 IntentFilter intentFilter= new IntentFilter(); @@ -158,23 +166,29 @@ public class HomeFragment extends BaseFragment { public void onNext(@NonNull Response response) { String body = (String) response.body(); Log.e("护理资讯", body); - JSONObject json = JSON.parseObject(body); - JSONObject data = json.getJSONObject("data"); - if (data != null) { - JSONArray realTimeInfoList = data.getJSONArray("realTimeInfoList"); - if (realTimeInfoList != null) { - List list = new ArrayList<>(); - for (int i = 0; i < realTimeInfoList.size(); i++) { - JSONObject item = realTimeInfoList.getJSONObject(i); - homeListBean bean = new homeListBean(); - bean.setContent(item.getString("title")); - bean.setPic(item.getString("realTimeInfoImage")); - bean.setUrl(item.getString("realTimeInfoUrl")); - list.add(bean); + try { + JSONObject json = JSON.parseObject(body); + JSONObject data = json.getJSONObject("data"); + if (data != null) { + JSONArray realTimeInfoList = data.getJSONArray("realTimeInfoList"); + if (realTimeInfoList != null && realTimeInfoList.size() > 0) { + List list = new ArrayList<>(); + for (int i = 0; i < realTimeInfoList.size(); i++) { + JSONObject item = realTimeInfoList.getJSONObject(i); + homeListBean bean = new homeListBean(); + bean.setContent(item.getString("title")); + bean.setPic(item.getString("realTimeInfoImage")); + bean.setUrl(item.getString("realTimeInfoUrl")); + bean.setArticleBody(item.getString("content")); + list.add(bean); + } + robCustomerInfoBean.setNursingInfoList(list); + adapter.notifyDataSetChanged(); } - robCustomerInfoBean.setNursingInfoList(list); - adapter.notifyDataSetChanged(); } + } catch (Exception e) { + Log.e("护理资讯", "解析失败", e); + toast("护理资讯数据异常"); } } diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/main/task/ViewHolderFive.java b/peizhen/app/src/main/java/com/ruilaizi/service/main/task/ViewHolderFive.java index 3b6e8d6..4d82af8 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/main/task/ViewHolderFive.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/main/task/ViewHolderFive.java @@ -67,8 +67,11 @@ public class ViewHolderFive extends AbstractViewTypeHolder { return; } String url = item.getUrl(); + String articleBody = item.getArticleBody(); if (url != null && !url.isEmpty()) { XfiveWebActivity.runActivity(mContext, "了解陪护", url); + } else if (articleBody != null && !articleBody.isEmpty()) { + XfiveWebActivity.runActivityWithContent(mContext, item.getContent(), articleBody); } else { XfiveWebActivity.runActivity(mContext, "了解陪护", "file:///android_asset/privacy.html"); } diff --git a/peizhen/app/src/main/java/com/ruilaizi/service/view/webview/XfiveWebActivity.java b/peizhen/app/src/main/java/com/ruilaizi/service/view/webview/XfiveWebActivity.java index 7ebc5d2..b1e6584 100644 --- a/peizhen/app/src/main/java/com/ruilaizi/service/view/webview/XfiveWebActivity.java +++ b/peizhen/app/src/main/java/com/ruilaizi/service/view/webview/XfiveWebActivity.java @@ -18,6 +18,7 @@ import com.ruilaizi.service.utils.X5WebView; public class XfiveWebActivity extends BaseActivity implements View.OnClickListener { public final static String URL = "url"; public final static String TITLE = "title"; + public final static String CONTENT = "content"; private RelativeLayout mLayTopLeftTv; private TextView mLayTopTitle; private X5WebView webview; @@ -29,14 +30,22 @@ public class XfiveWebActivity extends BaseActivity implements View.OnClickListen context.startActivity(intent); } + public static void runActivityWithContent(Context context, String title, String htmlContent) { + Intent intent = new Intent(context, XfiveWebActivity.class); + intent.putExtra(TITLE, title); + intent.putExtra(CONTENT, htmlContent); + context.startActivity(intent); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_xfive); String url = getIntent().getStringExtra(URL); + String content = getIntent().getStringExtra(CONTENT); String title = getIntent().getStringExtra(TITLE); initView(title); - initData(url); + initData(url, content); } @@ -54,7 +63,7 @@ public class XfiveWebActivity extends BaseActivity implements View.OnClickListen } - private void initData(String url) { + private void initData(String url, String htmlContent) { webview = (X5WebView) findViewById(R.id.webView); webview.getView().setOverScrollMode(View.OVER_SCROLL_ALWAYS); webview.addJavascriptInterface(new WebViewJavaScriptFunction() { @@ -66,8 +75,11 @@ public class XfiveWebActivity extends BaseActivity implements View.OnClickListen } }, "Android"); - //加载网页 - webview.loadUrl(url); + if (htmlContent != null && !htmlContent.isEmpty()) { + webview.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); + } else if (url != null && !url.isEmpty()) { + webview.loadUrl(url); + } } diff --git a/rlz/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/rlz/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java index ddc0e5f..86db12a 100644 --- a/rlz/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java +++ b/rlz/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -388,6 +388,7 @@ public class SysUserController extends BaseController item.put("title", article.getTitle()); item.put("realTimeInfoUrl", article.getArticleUrl()); item.put("realTimeInfoImage", article.getImageUrl()); + item.put("content", article.getContent()); realTimeInfoList.add(item); } }