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 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-23 01:29:43 +08:00
parent fe5a2f2fdc
commit 7d56ff4bfe
7 changed files with 347 additions and 20 deletions

View File

@@ -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 编辑器(`<editor>` 全局组件),支持:
- 加粗、斜体、下划线、删除线
- 标题 H1-H6
- 有序/无序列表、缩进
- 文字颜色、背景色
- 链接、图片、视频插入
- 图片通过 `/common/upload` 上传到 OSS
### 图片上传
封面图片使用 `<image-upload>` 全局组件:
- 支持 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 全局组件可直接使用**`<editor>` / `<image-upload>` / `<image-preview>` 在 main.js 已注册,页面中直接写标签即可,无需 import。
6. **Windows git 推送 Gitea 偶现超时** — 可能需要更换 SSH 端口或使用 HTTP 协议。本项目中通过 paramiko SFTP 直传文件作为备用部署方案。
7. **OkGo 的 REQUEST_FAILED_READ_CACHE 模式** — 首次请求成功后会缓存响应,后续网络失败时自动读缓存,适合首页不太频繁变动的数据。

View File

@@ -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
- 封面图片改为 `<image-upload>` 上传组件(替代手动贴 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` | 新增 |

View File

@@ -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;
}
}

View File

@@ -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<homeListBean> 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<String> 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<homeListBean> 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<homeListBean> 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("护理资讯数据异常");
}
}

View File

@@ -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");
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}