From 369898bd7cb7cefcd76c95e85269d6bc1638b73e Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Thu, 24 Sep 2026 00:02:03 +0800 Subject: [PATCH] feat: integrate cultivate admin internal APIs --- ...vate-Server管理后台内部接口开发对接文档.md | 696 ++++++++++++++++ server/hack/config.yaml | 11 - server/internal/dao/admin_operation_log.go | 22 - server/internal/dao/content_audit_log.go | 22 - server/internal/dao/feed_comment.go | 22 - server/internal/dao/feed_media.go | 22 - server/internal/dao/feed_post.go | 22 - server/internal/dao/feed_report.go | 22 - server/internal/dao/flower_catalog.go | 22 - server/internal/dao/flower_stage_rule.go | 22 - .../dao/internal/admin_operation_log.go | 103 --- .../dao/internal/content_audit_log.go | 103 --- server/internal/dao/internal/feed_comment.go | 105 --- server/internal/dao/internal/feed_media.go | 105 --- server/internal/dao/internal/feed_post.go | 117 --- server/internal/dao/internal/feed_report.go | 109 --- .../internal/dao/internal/flower_catalog.go | 109 --- .../dao/internal/flower_stage_rule.go | 109 --- .../internal/library/cultivateadmin/client.go | 304 +++++++ .../library/cultivateadmin/client_test.go | 105 +++ .../internal/logic/sys/cultivate_content.go | 741 +++--------------- server/internal/logic/sys/cultivate_flower.go | 429 +++++----- .../internal/model/do/admin_operation_log.go | 29 - server/internal/model/do/content_audit_log.go | 29 - server/internal/model/do/feed_comment.go | 29 - server/internal/model/do/feed_media.go | 29 - server/internal/model/do/feed_post.go | 35 - server/internal/model/do/feed_report.go | 32 - server/internal/model/do/flower_catalog.go | 32 - server/internal/model/do/flower_stage_rule.go | 32 - .../model/entity/admin_operation_log.go | 27 - .../model/entity/content_audit_log.go | 27 - server/internal/model/entity/feed_comment.go | 27 - server/internal/model/entity/feed_media.go | 27 - server/internal/model/entity/feed_post.go | 33 - server/internal/model/entity/feed_report.go | 30 - .../internal/model/entity/flower_catalog.go | 30 - .../model/entity/flower_stage_rule.go | 30 - server/manifest/config/config.template | 8 +- 39 files changed, 1426 insertions(+), 2382 deletions(-) create mode 100644 docs/im/Cultivate-Server管理后台内部接口开发对接文档.md delete mode 100644 server/internal/dao/admin_operation_log.go delete mode 100644 server/internal/dao/content_audit_log.go delete mode 100644 server/internal/dao/feed_comment.go delete mode 100644 server/internal/dao/feed_media.go delete mode 100644 server/internal/dao/feed_post.go delete mode 100644 server/internal/dao/feed_report.go delete mode 100644 server/internal/dao/flower_catalog.go delete mode 100644 server/internal/dao/flower_stage_rule.go delete mode 100644 server/internal/dao/internal/admin_operation_log.go delete mode 100644 server/internal/dao/internal/content_audit_log.go delete mode 100644 server/internal/dao/internal/feed_comment.go delete mode 100644 server/internal/dao/internal/feed_media.go delete mode 100644 server/internal/dao/internal/feed_post.go delete mode 100644 server/internal/dao/internal/feed_report.go delete mode 100644 server/internal/dao/internal/flower_catalog.go delete mode 100644 server/internal/dao/internal/flower_stage_rule.go create mode 100644 server/internal/library/cultivateadmin/client.go create mode 100644 server/internal/library/cultivateadmin/client_test.go delete mode 100644 server/internal/model/do/admin_operation_log.go delete mode 100644 server/internal/model/do/content_audit_log.go delete mode 100644 server/internal/model/do/feed_comment.go delete mode 100644 server/internal/model/do/feed_media.go delete mode 100644 server/internal/model/do/feed_post.go delete mode 100644 server/internal/model/do/feed_report.go delete mode 100644 server/internal/model/do/flower_catalog.go delete mode 100644 server/internal/model/do/flower_stage_rule.go delete mode 100644 server/internal/model/entity/admin_operation_log.go delete mode 100644 server/internal/model/entity/content_audit_log.go delete mode 100644 server/internal/model/entity/feed_comment.go delete mode 100644 server/internal/model/entity/feed_media.go delete mode 100644 server/internal/model/entity/feed_post.go delete mode 100644 server/internal/model/entity/feed_report.go delete mode 100644 server/internal/model/entity/flower_catalog.go delete mode 100644 server/internal/model/entity/flower_stage_rule.go diff --git a/docs/im/Cultivate-Server管理后台内部接口开发对接文档.md b/docs/im/Cultivate-Server管理后台内部接口开发对接文档.md new file mode 100644 index 0000000..81d63ca --- /dev/null +++ b/docs/im/Cultivate-Server管理后台内部接口开发对接文档.md @@ -0,0 +1,696 @@ +# Cultivate Server 管理后台内部接口开发对接文档 + +## 1. 文档目的 + +本文用于指导 `cultivate-server` 开发管理后台内部接口,使 IM Manager 可以通过服务间 HTTP API 管理养成业务,不再直接连接 Cultivate 数据库。 + +改造完成后必须满足: + +1. IM Manager 不配置 `CULTIVATE_MYSQL_DSN`,也不持有 Cultivate 数据库账号。 +2. Cultivate 数据只能由 `cultivate-server` 读写。 +3. Manager 只负责管理员登录态、RBAC、页面协议转换和调用内部接口。 +4. Cultivate 服务不可用时,只影响养成管理菜单,不得影响 Manager 启动及其他菜单。 +5. 所有查询和写入必须按商户隔离,禁止信任前端传入的 `tenant_id`、`operator_id`。 + +## 2. 当前问题 + +当前 IM Manager 在配置中注册了独立数据库组: + +```yaml +database: + cultivate: + link: "$CULTIVATE_MYSQL_DSN" +``` + +养成管理逻辑通过 `g.DB("cultivate")` 及 group 为 `cultivate` 的 DAO 直接访问以下表: + +- `cv_flower_catalog` +- `cv_flower_stage_rule` +- `cv_user_flower` +- `cv_garden_active_day` +- `cv_feed_post` +- `cv_feed_comment` +- `cv_feed_media` +- `cv_feed_report` +- `cv_content_audit_log` +- `cv_admin_operation_log` + +该实现造成: + +- Manager 启动依赖 Cultivate 数据库配置。 +- Cultivate 数据库账号扩散到 Manager。 +- Cultivate 表结构变化会直接影响 Manager。 +- 内容审核、通知、文件校验等事务边界跨服务泄漏。 + +本次应将上述数据库逻辑迁移到 `cultivate-server`,不能以给 Manager 增加数据库 Secret 的方式规避。 + +## 3. 服务职责 + +### 3.1 Cultivate Server + +负责: + +- 查询养成概览。 +- 花种及成长阶段配置的增删改查和状态变更。 +- 动态、评论审核列表、详情和人工审核。 +- 举报列表、详情和处理。 +- 管理操作日志查询和写入。 +- 调用 IM 内部接口查询用户摘要、校验文件、发送系统通知及执行用户治理。 +- 数据事务、幂等、租户隔离和审计。 + +### 3.2 IM Manager + +负责: + +- 校验后台管理员登录态和菜单权限。 +- 从登录态解析商户、操作人和角色。 +- 对 Cultivate 内部请求签名。 +- 将 Cultivate 返回结果转换为现有后台页面响应。 +- 将下游不可用错误转换成清晰的中文业务错误。 + +Manager 不再保存 Cultivate DAO、实体模型或数据库连接。 + +## 4. 接口基础约定 + +### 4.1 路径 + +内部管理接口统一使用: + +```text +/internal/admin/cultivate/tenants/{tenant_code}/... +``` + +示例: + +```text +GET /internal/admin/cultivate/tenants/default/dashboard +``` + +这些接口只能在集群内访问,不挂到公网 Ingress。 + +### 4.2 数据格式 + +- 请求和响应均使用 UTF-8 JSON。 +- JSON 字段统一使用 `snake_case`。 +- ID 统一使用十进制字符串返回,避免 JavaScript 超出安全整数范围。 +- 时间统一使用 UTC RFC3339,例如 `2026-09-23T08:30:00Z`。 +- 分页参数为 `page`、`per_page`,默认分别为 `1`、`20`。 +- `per_page` 最大为 `100`。 + +统一成功响应: + +```json +{ + "code": 0, + "data": {}, + "server_time": "2026-09-23T08:30:00Z", + "request_id": "req_xxx" +} +``` + +统一失败响应: + +```json +{ + "code": 40001, + "reason": "INVALID_ARGUMENT", + "message": "内容类型不正确", + "details": null, + "server_time": "2026-09-23T08:30:00Z", + "request_id": "req_xxx" +} +``` + +分页数据统一放在 `data` 中: + +```json +{ + "items": [], + "total": 0, + "page": 1, + "per_page": 20 +} +``` + +## 5. 内部鉴权 + +### 5.1 请求头 + +| 请求头 | 必填 | 说明 | +| --- | --- | --- | +| `X-Service-Name` | 是 | 调用方服务名,Manager 固定为 `im-manager` | +| `X-Timestamp` | 是 | UTC Unix 秒 | +| `X-Nonce` | 是 | 每次请求随机值,服务端防重放 | +| `X-Signature` | 是 | HMAC-SHA256 十六进制小写字符串 | +| `X-Tenant-ID` | 是 | Manager 登录态解析出的商户 ID | +| `X-Operator-ID` | 是 | Manager 登录态解析出的管理员 ID | +| `X-Role-Key` | 否 | 管理员角色标识,用于审计,不作为唯一授权依据 | +| `X-Request-ID` | 是 | 全链路请求 ID | + +### 5.2 签名串 + +双方复用现有内部服务密钥,不新增数据库相关 Secret。签名原文为: + +```text +METHOD\n +REQUEST_URI\n +SHA256_HEX(BODY)\n +TIMESTAMP\n +NONCE\n +TENANT_ID\n +OPERATOR_ID\n +ROLE_KEY\n +REQUEST_ID +``` + +说明: + +- `REQUEST_URI` 包含路径和排序后的 Query String。 +- GET 请求的 Body 为空字节数组。 +- 使用共享密钥计算 `HMAC-SHA256`。 +- 时间窗口建议为正负 300 秒。 +- `X-Service-Name + X-Nonce` 在时间窗口内只能使用一次。 +- 必须使用常量时间比较签名。 +- 商户、操作人及角色头已经纳入签名,业务层不得接受 Body 中同名字段覆盖。 + +### 5.3 商户校验 + +1. `{tenant_code}` 必须与签名后的 `X-Tenant-ID` 对应。 +2. 所有 SQL 必须带 `tenant_id = X-Tenant-ID`。 +3. 查不到商户或编码与 ID 不匹配时返回 `TENANT_NOT_FOUND` 或 `TENANT_MISMATCH`。 +4. 禁止使用默认商户兜底处理错误的商户参数。 + +## 6. 接口清单 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/dashboard` | 养成管理概览 | +| GET | `/flowers` | 花种列表 | +| GET | `/flowers/{id}` | 花种详情 | +| POST | `/flowers` | 新建花种 | +| PUT | `/flowers/{id}` | 编辑花种 | +| POST | `/flowers/{id}/status` | 花种状态变更 | +| GET | `/content-reviews` | 内容审核列表 | +| GET | `/content-reviews/{object_type}/{id}` | 内容审核详情 | +| POST | `/content-reviews/{object_type}/{id}/actions` | 单条审核操作 | +| POST | `/content-reviews/batch-actions` | 批量审核操作 | +| GET | `/reports` | 举报列表 | +| GET | `/reports/{id}` | 举报详情 | +| POST | `/reports/{id}/actions` | 处理举报 | +| GET | `/operation-logs` | 操作日志列表 | + +以下章节中的路径均省略前缀: + +```text +/internal/admin/cultivate/tenants/{tenant_code} +``` + +## 7. 数据概览 + +### 7.1 `GET /dashboard` + +响应 `data`: + +```json +{ + "plant_count": 0, + "growing_count": 0, + "bloom_count": 0, + "harvested_count": 0, + "removed_count": 0, + "active_7d": 0, + "active_30d": 0, + "post_count": 0, + "public_post_count": 0, + "friends_post_count": 0, + "private_post_count": 0, + "comment_count": 0, + "like_count": 0, + "taken_down_count": 0, + "pending_audit_count": 0, + "approved_audit_count": 0, + "rejected_audit_count": 0, + "pending_report_count": 0, + "processing_report_count": 0, + "processed_report_count": 0 +} +``` + +统计口径必须沿用当前业务状态,且全部按商户过滤。没有数据时返回 `0`,不能返回 `null`。 + +## 8. 花种管理 + +### 8.1 `GET /flowers` + +Query 参数: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `flower_code` | string | 编码模糊查询 | +| `flower_name` | string | 名称模糊查询 | +| `status` | string | 状态精确查询 | +| `config_version` | string | 配置版本 | +| `created_from` | RFC3339 | 创建开始时间 | +| `created_to` | RFC3339 | 创建结束时间 | +| `page` | int | 页码 | +| `per_page` | int | 每页数量 | + +列表项: + +```json +{ + "id": "10001", + "flower_code": "rose", + "flower_name": "玫瑰", + "flower_desc": "", + "icon_file_id": "file_xxx", + "status": "online", + "config_version": "cat_20260923_163000", + "daily_water_limit": 3, + "daily_fertilize_limit": 1, + "sort_no": 10, + "stage_count": 4, + "created_at": "2026-09-23T08:30:00Z", + "updated_at": "2026-09-23T08:30:00Z" +} +``` + +排序规则:`sort_no ASC, id DESC`。 + +### 8.2 `GET /flowers/{id}` + +在列表字段基础上增加: + +```json +{ + "share_config": {}, + "stage_rules": [ + { + "id": "10002", + "stage": "seed", + "next_stage": "sprout", + "required_points": 10, + "water_points": 2, + "fertilize_points": 3, + "allowed_actions": ["water", "fertilize"], + "image_file_id": "file_xxx", + "animation_file_id": "file_xxx", + "progress_text_template": "成长中", + "completion_text": "已进入发芽阶段", + "sort_no": 10 + } + ] +} +``` + +### 8.3 `POST /flowers` 与 `PUT /flowers/{id}` + +请求体: + +```json +{ + "flower_code": "rose", + "flower_name": "玫瑰", + "flower_desc": "", + "icon_file_id": "file_xxx", + "status": "offline", + "daily_water_limit": 3, + "daily_fertilize_limit": 1, + "share_config": {}, + "sort_no": 10, + "stage_rules": [] +} +``` + +业务规则: + +- `flower_code`、`flower_name` 必填。 +- `flower_code` 创建后不可修改。 +- 状态枚举:`online`、`offline`、`activity_only`、`maintenance`。 +- 阶段枚举:`seed`、`sprout`、`bud`、`bloom`。 +- 阶段链必须从 `seed` 连续配置并以 `bloom` 结束,不能重复或成环。 +- 非 `bloom` 阶段 `required_points > 0`,且至少允许一个动作。 +- 动作枚举:`water`、`fertilize`。 +- 文件 ID 必须通过 IM 内部文件校验,风险或待审核素材不能保存。 +- 花种和阶段规则必须在一个数据库事务内保存。 +- 每次新增或编辑生成新的 `config_version` 并写管理操作日志。 +- 新建返回 `data.id`,ID 使用字符串。 + +### 8.4 `POST /flowers/{id}/status` + +```json +{ + "status": "online", + "reason": "审核通过后上架" +} +``` + +更新上下架时间、配置版本并写操作日志。 + +## 9. 内容审核 + +### 9.1 `GET /content-reviews` + +Query 参数: + +- `object_type`:必填,`feed_post` 或 `feed_comment`。 +- `author_id`:作者 ID。 +- `status`:业务状态。 +- `keyword`:正文关键字。 +- `created_from`、`created_to`。 +- `page`、`per_page`。 + +列表项: + +```json +{ + "id": "20001", + "object_type": "feed_post", + "post_id": "", + "author_id": "30001", + "content_text": "正文", + "flower_code": "rose", + "visibility": "public", + "status": "reviewing", + "audit_status": "pending", + "reject_reason": "", + "like_count": 0, + "comment_count": 0, + "created_at": "2026-09-23T08:30:00Z", + "updated_at": "2026-09-23T08:30:00Z" +} +``` + +按 `id DESC` 排序。 + +### 9.2 `GET /content-reviews/{object_type}/{id}` + +详情在列表项基础上增加: + +- `author_nickname` +- `author_avatar_file_id` +- `media[]`:`id`、`file_id`、`thumbnail_file_id`、`media_type`、`audit_status`、`sort_no` +- `audits[]`:`id`、`audit_source`、`status_before`、`status_after`、`result_code`、`risk_labels`、`operator_id`、`occurred_at` +- `related_reports[]`:关联举报摘要 + +用户摘要通过 IM 内部 API 查询。用户服务临时不可用时,允许昵称和头像为空,但内容主体仍应返回。 + +### 9.3 `POST /content-reviews/{object_type}/{id}/actions` + +```json +{ + "action": "approve", + "reason": "" +} +``` + +动作枚举: + +- `approve`:审核通过。 +- `reject`:审核拒绝,必须填写原因。 +- `take_down`:下架,必须填写原因。 +- `restore`:恢复内容。 + +要求: + +- 使用行锁或等价并发控制。 +- 校验合法的前置状态。 +- 更新内容状态和审核字段。 +- 评论首次通过时只能增加一次动态评论数。 +- 下架动态时应按现有业务规则同步处理其可见性或关联评论。 +- 每次操作写 `cv_content_audit_log` 和 `cv_admin_operation_log`。 +- 事务提交后向作者发送系统通知。 +- 重复请求不能重复计数或重复产生治理副作用。 + +### 9.4 `POST /content-reviews/batch-actions` + +```json +{ + "object_type": "feed_post", + "ids": ["20001", "20002"], + "action": "approve", + "reason": "" +} +``` + +- 单次 1 至 100 条。 +- 每条独立处理,单条失败不回滚其他成功项。 +- 返回: + +```json +{ + "success_count": 1, + "failed_count": 1, + "failures": { + "20002": "当前状态不允许审核通过" + } +} +``` + +## 10. 举报管理 + +### 10.1 `GET /reports` + +Query 参数: + +- `object_type` +- `status` +- `reporter_id` +- `created_from`、`created_to` +- `page`、`per_page` + +列表项: + +```json +{ + "id": "40001", + "reporter_id": "30001", + "reported_user_id": "30002", + "object_type": "feed_post", + "object_id": "20001", + "reason_code": "spam", + "description": "疑似广告", + "evidence_file_ids": ["file_xxx"], + "source_page": "feed_detail", + "status": "submitted", + "handler_id": "", + "handle_result": "", + "handled_at": null, + "created_at": "2026-09-23T08:30:00Z" +} +``` + +### 10.2 `GET /reports/{id}` + +详情增加: + +- `reporter_nickname` +- `reported_user_nickname` +- `target_content` +- `history[]`:同一对象的历史举报 + +不得向被举报方的通知或普通业务接口泄露举报人身份和证据。 + +### 10.3 `POST /reports/{id}/actions` + +```json +{ + "action": "resolve", + "handle_result": "举报成立,内容已下架", + "content_action": "take_down", + "user_action": "none", + "reason": "违规广告" +} +``` + +枚举: + +- `action`:`processing`、`resolve`、`reject`。 +- `content_action`:`none`、`take_down`。 +- `user_action`:`none`、`freeze`、`ban`。 + +校验规则: + +- `processing` 不要求处理结果。 +- `resolve`、`reject` 必须填写 `handle_result`。 +- 只有 `resolve` 可以联动内容和用户处置。 +- 发生联动处置时必须填写 `reason`。 +- 报告状态更新、内容处置和操作日志应处于同一事务。 +- 用户冻结或封禁通过 IM 内部治理 API 执行;失败时不得将举报错误标记为已完成。 +- 完成后分别通知举报方和被举报方,被举报方通知不得携带举报人信息。 + +## 11. 操作日志 + +### 11.1 `GET /operation-logs` + +Query 参数: + +- `operator_id` +- `operation_type` +- `target_type` +- `target_id` +- `request_id` +- `created_from`、`created_to` +- `page`、`per_page` + +列表项: + +```json +{ + "id": "50001", + "operator_id": "14", + "operation_type": "content.approve", + "target_type": "feed_post", + "target_id": "20001", + "request_id": "req_xxx", + "before_snapshot": {}, + "after_snapshot": {}, + "reason": "", + "created_at": "2026-09-23T08:30:00Z" +} +``` + +日志只读,不提供删除接口。快照不得保存 Token、手机号、数据库连接、完整举报证据或私有文件 URL。 + +## 12. 状态和枚举 + +| 类型 | 枚举 | +| --- | --- | +| 内容类型 | `feed_post`、`feed_comment` | +| 花种状态 | `online`、`offline`、`activity_only`、`maintenance` | +| 成长阶段 | `seed`、`sprout`、`bud`、`bloom` | +| 成长动作 | `water`、`fertilize` | +| 可见范围 | `public`、`friends`、`private` | +| 内容状态 | `reviewing`、`published`、`rejected`、`taken_down` | +| 审核动作 | `approve`、`reject`、`take_down`、`restore` | +| 举报状态 | `submitted`、`processing`、`resolved`、`rejected` | +| 举报动作 | `processing`、`resolve`、`reject` | +| 用户处置 | `none`、`freeze`、`ban` | + +如果现有数据库已经使用其他稳定枚举,开发前应与 Manager 对齐,不允许接口层静默转换出第三套状态。 + +## 13. 错误码建议 + +| HTTP | reason | 场景 | +| --- | --- | --- | +| 400 | `INVALID_ARGUMENT` | 参数或枚举错误 | +| 401 | `INTERNAL_AUTH_REQUIRED` | 缺少内部签名 | +| 403 | `INTERNAL_AUTH_FORBIDDEN` | 签名错误、服务未授权 | +| 404 | `TENANT_NOT_FOUND` | 商户不存在 | +| 404 | `RESOURCE_NOT_FOUND` | 花种、内容或举报不存在 | +| 409 | `STATE_CONFLICT` | 当前状态不允许执行动作 | +| 409 | `DUPLICATE_FLOWER_CODE` | 商户内花种编码重复 | +| 422 | `ASSET_REVIEW_PENDING` | 素材仍在审核中 | +| 422 | `ASSET_REVIEW_REJECTED` | 素材审核未通过 | +| 429 | `RATE_LIMITED` | 调用频率过高 | +| 503 | `DEPENDENCY_UNAVAILABLE` | IM 用户、文件、通知或治理服务不可用 | +| 500 | `INTERNAL_ERROR` | 未知内部错误 | + +响应不得返回 SQL、数据库地址、堆栈或 Secret。 + +## 14. 数据一致性与安全要求 + +1. 每条 SQL 都必须显式过滤 `tenant_id`。 +2. 更新语句同时按主键和 `tenant_id` 定位。 +3. 写接口记录签名后的 `X-Operator-ID` 和 `X-Request-ID`。 +4. 审核、举报处置和计数变更必须考虑重试幂等。 +5. 批量操作最多 100 条,避免长事务。 +6. 私有媒体只返回 `file_id`,访问地址由受控文件接口临时签发。 +7. 关键操作日志永久保留;普通查询对已注销用户做匿名化。 +8. 内部接口不可复用 C 端 Token 鉴权,必须单独使用服务间 HMAC。 +9. 内部 API 路由应在普通 C 端认证中间件之前单独注册,不能误走 Redis 用户会话认证。 + +## 15. Cultivate Server 建议目录 + +按现有分层新增,禁止在 controller 中写数据库逻辑: + +```text +api/internal/admin/ + cultivate.go +internal/controller/admin/ + cultivate.go +internal/service/ + admin_cultivate.go +internal/logic/admin/ + dashboard.go + flower.go + content_review.go + report.go + operation_log.go +internal/middleware/ + internal_admin_auth.go +internal/model/ + admin_cultivate.go +``` + +路由建议独立注册: + +```text +/internal/admin/cultivate/* +``` + +不要将管理接口混入 `/v1/cultivate/*` C 端路由。 + +## 16. Manager 后续改造 + +Cultivate Server 接口完成后,Manager 侧将执行: + +1. 新增 Cultivate 内部 API Client。 +2. 将现有页面 API 转发到本文定义的内部接口。 +3. 删除 Manager 内的 Cultivate 直接数据库逻辑。 +4. 删除 Cultivate DAO、DO、Entity 等仅为直连数据库生成的代码。 +5. 删除配置模板中的 `database.cultivate` 和 `CULTIVATE_MYSQL_DSN`。 +6. 保持现有后台页面路径和前端交互不变。 + +服务地址建议支持配置,但 Kubernetes 环境可提供集群内默认值: + +```text +http://cultivate-server.cultivate-system.svc.cluster.local:8080 +``` + +该地址不是数据库配置。密钥复用双方已有内部 HMAC Secret,不增加 Cultivate 数据库 Secret。 + +## 17. 测试要求 + +### 17.1 单元测试 + +- HMAC 正确、错误、过期和 Nonce 重放。 +- 商户编码与 ID 不匹配。 +- 所有列表过滤和分页。 +- 花种阶段链缺失、重复、成环和合法场景。 +- 内容每种状态动作的合法与非法转换。 +- 重复审核不重复增加评论数。 +- 举报各种状态和联动处置。 +- 跨商户 ID 查询和更新均返回不存在。 + +### 17.2 集成测试 + +- 使用真实 Cultivate 测试库验证全部接口。 +- 模拟 IM 用户、文件、通知和治理内部接口。 +- 验证事务失败不会留下半完成数据。 +- 验证操作日志中的商户、管理员和请求 ID 正确。 +- 验证接口响应不包含数据库信息和堆栈。 + +### 17.3 联调验收 + +1. Manager 不设置 `CULTIVATE_MYSQL_DSN` 可以正常启动。 +2. Manager 登录、IM 管理和工作台管理均正常。 +3. 养成概览、花种、内容审核、举报和操作日志页面可正常查询。 +4. 花种新增、编辑、上下架生效。 +5. 内容审核和举报处置生效,通知与审计记录正确。 +6. 停止 Cultivate Server 后,Manager 仍保持健康,仅养成页面提示“养成服务暂不可用”。 +7. Manager Pod 环境变量和 Secret 中不存在 Cultivate 数据库 DSN。 + +## 18. 交付清单 + +Cultivate Server 开发完成后需提供: + +- 接口代码及单元测试。 +- OpenAPI 或等价接口定义。 +- 内部 HMAC 鉴权说明。 +- 测试环境服务地址。 +- 联调使用的服务名和 Secret 键名,不提供明文 Secret。 +- 数据迁移说明,如无迁移需明确说明。 +- 一份 curl 签名测试脚本或 Go 测试客户端。 + +上述内容齐备后,再开始 Manager 侧切换和删除直连代码。 diff --git a/server/hack/config.yaml b/server/hack/config.yaml index 5b08599..952025e 100644 --- a/server/hack/config.yaml +++ b/server/hack/config.yaml @@ -34,17 +34,6 @@ gfcli: gJsonSupport: true clear: false - - link: "mysql:root:root@tcp(127.0.0.1:3306)/cultivate?loc=Local&parseTime=true&charset=utf8mb4" - group: "cultivate" - tables: "cv_flower_catalog,cv_flower_stage_rule,cv_feed_post,cv_feed_media,cv_feed_comment,cv_feed_report,cv_content_audit_log,cv_admin_operation_log" - removePrefix: "cv_" - descriptionTag: true - noModelComment: true - jsonCase: "CamelLower" - gJsonSupport: true - clear: false - - # service: # 生成业务配置 # srcFolder: "internal/logic" # dstFolder: "internal/service" diff --git a/server/internal/dao/admin_operation_log.go b/server/internal/dao/admin_operation_log.go deleted file mode 100644 index 634f918..0000000 --- a/server/internal/dao/admin_operation_log.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// adminOperationLogDao is the data access object for the table cv_admin_operation_log. -// You can define custom methods on it to extend its functionality as needed. -type adminOperationLogDao struct { - *internal.AdminOperationLogDao -} - -var ( - // AdminOperationLog is a globally accessible object for table cv_admin_operation_log operations. - AdminOperationLog = adminOperationLogDao{internal.NewAdminOperationLogDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/content_audit_log.go b/server/internal/dao/content_audit_log.go deleted file mode 100644 index 437e3c6..0000000 --- a/server/internal/dao/content_audit_log.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// contentAuditLogDao is the data access object for the table cv_content_audit_log. -// You can define custom methods on it to extend its functionality as needed. -type contentAuditLogDao struct { - *internal.ContentAuditLogDao -} - -var ( - // ContentAuditLog is a globally accessible object for table cv_content_audit_log operations. - ContentAuditLog = contentAuditLogDao{internal.NewContentAuditLogDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/feed_comment.go b/server/internal/dao/feed_comment.go deleted file mode 100644 index 22dda10..0000000 --- a/server/internal/dao/feed_comment.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// feedCommentDao is the data access object for the table cv_feed_comment. -// You can define custom methods on it to extend its functionality as needed. -type feedCommentDao struct { - *internal.FeedCommentDao -} - -var ( - // FeedComment is a globally accessible object for table cv_feed_comment operations. - FeedComment = feedCommentDao{internal.NewFeedCommentDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/feed_media.go b/server/internal/dao/feed_media.go deleted file mode 100644 index 80af1b8..0000000 --- a/server/internal/dao/feed_media.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// feedMediaDao is the data access object for the table cv_feed_media. -// You can define custom methods on it to extend its functionality as needed. -type feedMediaDao struct { - *internal.FeedMediaDao -} - -var ( - // FeedMedia is a globally accessible object for table cv_feed_media operations. - FeedMedia = feedMediaDao{internal.NewFeedMediaDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/feed_post.go b/server/internal/dao/feed_post.go deleted file mode 100644 index 87a4432..0000000 --- a/server/internal/dao/feed_post.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// feedPostDao is the data access object for the table cv_feed_post. -// You can define custom methods on it to extend its functionality as needed. -type feedPostDao struct { - *internal.FeedPostDao -} - -var ( - // FeedPost is a globally accessible object for table cv_feed_post operations. - FeedPost = feedPostDao{internal.NewFeedPostDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/feed_report.go b/server/internal/dao/feed_report.go deleted file mode 100644 index a8124e5..0000000 --- a/server/internal/dao/feed_report.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// feedReportDao is the data access object for the table cv_feed_report. -// You can define custom methods on it to extend its functionality as needed. -type feedReportDao struct { - *internal.FeedReportDao -} - -var ( - // FeedReport is a globally accessible object for table cv_feed_report operations. - FeedReport = feedReportDao{internal.NewFeedReportDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/flower_catalog.go b/server/internal/dao/flower_catalog.go deleted file mode 100644 index 48858d6..0000000 --- a/server/internal/dao/flower_catalog.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// flowerCatalogDao is the data access object for the table cv_flower_catalog. -// You can define custom methods on it to extend its functionality as needed. -type flowerCatalogDao struct { - *internal.FlowerCatalogDao -} - -var ( - // FlowerCatalog is a globally accessible object for table cv_flower_catalog operations. - FlowerCatalog = flowerCatalogDao{internal.NewFlowerCatalogDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/flower_stage_rule.go b/server/internal/dao/flower_stage_rule.go deleted file mode 100644 index 6e6957d..0000000 --- a/server/internal/dao/flower_stage_rule.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "hotgo/internal/dao/internal" -) - -// flowerStageRuleDao is the data access object for the table cv_flower_stage_rule. -// You can define custom methods on it to extend its functionality as needed. -type flowerStageRuleDao struct { - *internal.FlowerStageRuleDao -} - -var ( - // FlowerStageRule is a globally accessible object for table cv_flower_stage_rule operations. - FlowerStageRule = flowerStageRuleDao{internal.NewFlowerStageRuleDao()} -) - -// Add your custom methods and functionality below. diff --git a/server/internal/dao/internal/admin_operation_log.go b/server/internal/dao/internal/admin_operation_log.go deleted file mode 100644 index 640b6d1..0000000 --- a/server/internal/dao/internal/admin_operation_log.go +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// AdminOperationLogDao is the data access object for the table cv_admin_operation_log. -type AdminOperationLogDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns AdminOperationLogColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// AdminOperationLogColumns defines and stores column names for the table cv_admin_operation_log. -type AdminOperationLogColumns struct { - Id string // - TenantId string // - OperatorId string // - OperationType string // - TargetType string // - TargetId string // - RequestId string // - BeforeSnapshot string // - AfterSnapshot string // - Reason string // - IpAddress string // - UserAgent string // - CreatedAt string // -} - -// adminOperationLogColumns holds the columns for the table cv_admin_operation_log. -var adminOperationLogColumns = AdminOperationLogColumns{ - Id: "id", - TenantId: "tenant_id", - OperatorId: "operator_id", - OperationType: "operation_type", - TargetType: "target_type", - TargetId: "target_id", - RequestId: "request_id", - BeforeSnapshot: "before_snapshot", - AfterSnapshot: "after_snapshot", - Reason: "reason", - IpAddress: "ip_address", - UserAgent: "user_agent", - CreatedAt: "created_at", -} - -// NewAdminOperationLogDao creates and returns a new DAO object for table data access. -func NewAdminOperationLogDao(handlers ...gdb.ModelHandler) *AdminOperationLogDao { - return &AdminOperationLogDao{ - group: "cultivate", - table: "cv_admin_operation_log", - columns: adminOperationLogColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *AdminOperationLogDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *AdminOperationLogDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *AdminOperationLogDao) Columns() AdminOperationLogColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *AdminOperationLogDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *AdminOperationLogDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *AdminOperationLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/content_audit_log.go b/server/internal/dao/internal/content_audit_log.go deleted file mode 100644 index 0a44a75..0000000 --- a/server/internal/dao/internal/content_audit_log.go +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// ContentAuditLogDao is the data access object for the table cv_content_audit_log. -type ContentAuditLogDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns ContentAuditLogColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// ContentAuditLogColumns defines and stores column names for the table cv_content_audit_log. -type ContentAuditLogColumns struct { - Id string // - TenantId string // - ObjectType string // - ObjectId string // - AuditRequestId string // - AuditSource string // - StatusBefore string // - StatusAfter string // - ResultCode string // - RiskLabels string // - OperatorId string // - OccurredAt string // - CreatedAt string // -} - -// contentAuditLogColumns holds the columns for the table cv_content_audit_log. -var contentAuditLogColumns = ContentAuditLogColumns{ - Id: "id", - TenantId: "tenant_id", - ObjectType: "object_type", - ObjectId: "object_id", - AuditRequestId: "audit_request_id", - AuditSource: "audit_source", - StatusBefore: "status_before", - StatusAfter: "status_after", - ResultCode: "result_code", - RiskLabels: "risk_labels", - OperatorId: "operator_id", - OccurredAt: "occurred_at", - CreatedAt: "created_at", -} - -// NewContentAuditLogDao creates and returns a new DAO object for table data access. -func NewContentAuditLogDao(handlers ...gdb.ModelHandler) *ContentAuditLogDao { - return &ContentAuditLogDao{ - group: "cultivate", - table: "cv_content_audit_log", - columns: contentAuditLogColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *ContentAuditLogDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *ContentAuditLogDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *ContentAuditLogDao) Columns() ContentAuditLogColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *ContentAuditLogDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *ContentAuditLogDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *ContentAuditLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/feed_comment.go b/server/internal/dao/internal/feed_comment.go deleted file mode 100644 index ca3fc1f..0000000 --- a/server/internal/dao/internal/feed_comment.go +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FeedCommentDao is the data access object for the table cv_feed_comment. -type FeedCommentDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FeedCommentColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FeedCommentColumns defines and stores column names for the table cv_feed_comment. -type FeedCommentColumns struct { - Id string // - TenantId string // - PostId string // - AuthorId string // - ContentText string // - Status string // - AuditStatus string // - AuditRequestId string // - DeletedBy string // - DeleteReason string // - PublishedAt string // - DeletedAt string // - CreatedAt string // - UpdatedAt string // -} - -// feedCommentColumns holds the columns for the table cv_feed_comment. -var feedCommentColumns = FeedCommentColumns{ - Id: "id", - TenantId: "tenant_id", - PostId: "post_id", - AuthorId: "author_id", - ContentText: "content_text", - Status: "status", - AuditStatus: "audit_status", - AuditRequestId: "audit_request_id", - DeletedBy: "deleted_by", - DeleteReason: "delete_reason", - PublishedAt: "published_at", - DeletedAt: "deleted_at", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFeedCommentDao creates and returns a new DAO object for table data access. -func NewFeedCommentDao(handlers ...gdb.ModelHandler) *FeedCommentDao { - return &FeedCommentDao{ - group: "cultivate", - table: "cv_feed_comment", - columns: feedCommentColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FeedCommentDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FeedCommentDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FeedCommentDao) Columns() FeedCommentColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FeedCommentDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FeedCommentDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FeedCommentDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/feed_media.go b/server/internal/dao/internal/feed_media.go deleted file mode 100644 index ea5e7c2..0000000 --- a/server/internal/dao/internal/feed_media.go +++ /dev/null @@ -1,105 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FeedMediaDao is the data access object for the table cv_feed_media. -type FeedMediaDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FeedMediaColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FeedMediaColumns defines and stores column names for the table cv_feed_media. -type FeedMediaColumns struct { - Id string // - TenantId string // - PostId string // - UserId string // - MediaType string // - FileId string // - ThumbnailFileId string // - Width string // - Height string // - SortNo string // - AuditStatus string // - AuditRequestId string // - CreatedAt string // - UpdatedAt string // -} - -// feedMediaColumns holds the columns for the table cv_feed_media. -var feedMediaColumns = FeedMediaColumns{ - Id: "id", - TenantId: "tenant_id", - PostId: "post_id", - UserId: "user_id", - MediaType: "media_type", - FileId: "file_id", - ThumbnailFileId: "thumbnail_file_id", - Width: "width", - Height: "height", - SortNo: "sort_no", - AuditStatus: "audit_status", - AuditRequestId: "audit_request_id", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFeedMediaDao creates and returns a new DAO object for table data access. -func NewFeedMediaDao(handlers ...gdb.ModelHandler) *FeedMediaDao { - return &FeedMediaDao{ - group: "cultivate", - table: "cv_feed_media", - columns: feedMediaColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FeedMediaDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FeedMediaDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FeedMediaDao) Columns() FeedMediaColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FeedMediaDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FeedMediaDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FeedMediaDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/feed_post.go b/server/internal/dao/internal/feed_post.go deleted file mode 100644 index f70fbaf..0000000 --- a/server/internal/dao/internal/feed_post.go +++ /dev/null @@ -1,117 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FeedPostDao is the data access object for the table cv_feed_post. -type FeedPostDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FeedPostColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FeedPostColumns defines and stores column names for the table cv_feed_post. -type FeedPostColumns struct { - Id string // - TenantId string // - AuthorId string // - ContentText string // - FlowerCode string // - SourceType string // - SourceId string // - Visibility string // - Status string // - AuditStatus string // - AuditRequestId string // - RejectReasonCode string // - LikeCount string // - CommentCount string // - Version string // - PublishedAt string // - DeletedAt string // - TakenDownAt string // - CreatedAt string // - UpdatedAt string // -} - -// feedPostColumns holds the columns for the table cv_feed_post. -var feedPostColumns = FeedPostColumns{ - Id: "id", - TenantId: "tenant_id", - AuthorId: "author_id", - ContentText: "content_text", - FlowerCode: "flower_code", - SourceType: "source_type", - SourceId: "source_id", - Visibility: "visibility", - Status: "status", - AuditStatus: "audit_status", - AuditRequestId: "audit_request_id", - RejectReasonCode: "reject_reason_code", - LikeCount: "like_count", - CommentCount: "comment_count", - Version: "version", - PublishedAt: "published_at", - DeletedAt: "deleted_at", - TakenDownAt: "taken_down_at", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFeedPostDao creates and returns a new DAO object for table data access. -func NewFeedPostDao(handlers ...gdb.ModelHandler) *FeedPostDao { - return &FeedPostDao{ - group: "cultivate", - table: "cv_feed_post", - columns: feedPostColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FeedPostDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FeedPostDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FeedPostDao) Columns() FeedPostColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FeedPostDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FeedPostDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FeedPostDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/feed_report.go b/server/internal/dao/internal/feed_report.go deleted file mode 100644 index 9da350d..0000000 --- a/server/internal/dao/internal/feed_report.go +++ /dev/null @@ -1,109 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FeedReportDao is the data access object for the table cv_feed_report. -type FeedReportDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FeedReportColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FeedReportColumns defines and stores column names for the table cv_feed_report. -type FeedReportColumns struct { - Id string // - TenantId string // - ReporterId string // - ReportedUserId string // - ObjectType string // - ObjectId string // - ReasonCode string // - Description string // - EvidenceFileIds string // - SourcePage string // - Status string // - HandlerId string // - HandleResult string // - HandledAt string // - CreatedAt string // - UpdatedAt string // -} - -// feedReportColumns holds the columns for the table cv_feed_report. -var feedReportColumns = FeedReportColumns{ - Id: "id", - TenantId: "tenant_id", - ReporterId: "reporter_id", - ReportedUserId: "reported_user_id", - ObjectType: "object_type", - ObjectId: "object_id", - ReasonCode: "reason_code", - Description: "description", - EvidenceFileIds: "evidence_file_ids", - SourcePage: "source_page", - Status: "status", - HandlerId: "handler_id", - HandleResult: "handle_result", - HandledAt: "handled_at", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFeedReportDao creates and returns a new DAO object for table data access. -func NewFeedReportDao(handlers ...gdb.ModelHandler) *FeedReportDao { - return &FeedReportDao{ - group: "cultivate", - table: "cv_feed_report", - columns: feedReportColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FeedReportDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FeedReportDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FeedReportDao) Columns() FeedReportColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FeedReportDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FeedReportDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FeedReportDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/flower_catalog.go b/server/internal/dao/internal/flower_catalog.go deleted file mode 100644 index fc0012e..0000000 --- a/server/internal/dao/internal/flower_catalog.go +++ /dev/null @@ -1,109 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FlowerCatalogDao is the data access object for the table cv_flower_catalog. -type FlowerCatalogDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FlowerCatalogColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FlowerCatalogColumns defines and stores column names for the table cv_flower_catalog. -type FlowerCatalogColumns struct { - Id string // - TenantId string // - FlowerCode string // - FlowerName string // - FlowerDesc string // - IconFileId string // - Status string // - ConfigVersion string // - DailyWaterLimit string // - DailyFertilizeLimit string // - ShareConfig string // - SortNo string // - OnlineAt string // - OfflineAt string // - CreatedAt string // - UpdatedAt string // -} - -// flowerCatalogColumns holds the columns for the table cv_flower_catalog. -var flowerCatalogColumns = FlowerCatalogColumns{ - Id: "id", - TenantId: "tenant_id", - FlowerCode: "flower_code", - FlowerName: "flower_name", - FlowerDesc: "flower_desc", - IconFileId: "icon_file_id", - Status: "status", - ConfigVersion: "config_version", - DailyWaterLimit: "daily_water_limit", - DailyFertilizeLimit: "daily_fertilize_limit", - ShareConfig: "share_config", - SortNo: "sort_no", - OnlineAt: "online_at", - OfflineAt: "offline_at", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFlowerCatalogDao creates and returns a new DAO object for table data access. -func NewFlowerCatalogDao(handlers ...gdb.ModelHandler) *FlowerCatalogDao { - return &FlowerCatalogDao{ - group: "cultivate", - table: "cv_flower_catalog", - columns: flowerCatalogColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FlowerCatalogDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FlowerCatalogDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FlowerCatalogDao) Columns() FlowerCatalogColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FlowerCatalogDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FlowerCatalogDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FlowerCatalogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/dao/internal/flower_stage_rule.go b/server/internal/dao/internal/flower_stage_rule.go deleted file mode 100644 index 94d6177..0000000 --- a/server/internal/dao/internal/flower_stage_rule.go +++ /dev/null @@ -1,109 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// FlowerStageRuleDao is the data access object for the table cv_flower_stage_rule. -type FlowerStageRuleDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns FlowerStageRuleColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// FlowerStageRuleColumns defines and stores column names for the table cv_flower_stage_rule. -type FlowerStageRuleColumns struct { - Id string // - TenantId string // - FlowerCatalogId string // - Stage string // - NextStage string // - RequiredPoints string // - WaterPoints string // - FertilizePoints string // - AllowedActions string // - ImageFileId string // - AnimationFileId string // - ProgressTextTemplate string // - CompletionText string // - SortNo string // - CreatedAt string // - UpdatedAt string // -} - -// flowerStageRuleColumns holds the columns for the table cv_flower_stage_rule. -var flowerStageRuleColumns = FlowerStageRuleColumns{ - Id: "id", - TenantId: "tenant_id", - FlowerCatalogId: "flower_catalog_id", - Stage: "stage", - NextStage: "next_stage", - RequiredPoints: "required_points", - WaterPoints: "water_points", - FertilizePoints: "fertilize_points", - AllowedActions: "allowed_actions", - ImageFileId: "image_file_id", - AnimationFileId: "animation_file_id", - ProgressTextTemplate: "progress_text_template", - CompletionText: "completion_text", - SortNo: "sort_no", - CreatedAt: "created_at", - UpdatedAt: "updated_at", -} - -// NewFlowerStageRuleDao creates and returns a new DAO object for table data access. -func NewFlowerStageRuleDao(handlers ...gdb.ModelHandler) *FlowerStageRuleDao { - return &FlowerStageRuleDao{ - group: "cultivate", - table: "cv_flower_stage_rule", - columns: flowerStageRuleColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *FlowerStageRuleDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *FlowerStageRuleDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *FlowerStageRuleDao) Columns() FlowerStageRuleColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *FlowerStageRuleDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *FlowerStageRuleDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *FlowerStageRuleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/server/internal/library/cultivateadmin/client.go b/server/internal/library/cultivateadmin/client.go new file mode 100644 index 0000000..d62648c --- /dev/null +++ b/server/internal/library/cultivateadmin/client.go @@ -0,0 +1,304 @@ +package cultivateadmin + +import ( + "bytes" + "context" + "crypto/hmac" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + "unicode" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" +) + +const ( + defaultBaseURL = "http://cultivate-server.cultivate-system.svc.cluster.local:8080" + maxBodySize = 10 << 20 +) + +type Config struct { + BaseURL string + ServiceName string + Secret string + Timeout time.Duration +} + +type Scope struct { + TenantID int64 + TenantCode string + OperatorID int64 + RoleKey string + RequestID string +} + +type Client struct { + conf Config + http *http.Client +} + +type Page[T any] struct { + Items []T `json:"items"` + Total int `json:"total"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} + +func DefaultClient(ctx context.Context) *Client { + timeout := g.Cfg().MustGet(ctx, "cultivateAdminApi.timeout", "5s").Duration() + if timeout <= 0 { + timeout = 5 * time.Second + } + baseURL := resolveValue(g.Cfg().MustGet(ctx, "cultivateAdminApi.baseUrl", defaultBaseURL).String()) + if baseURL == "" { + baseURL = defaultBaseURL + } + secret := firstValue( + g.Cfg().MustGet(ctx, "cultivateAdminApi.serviceSecret").String(), + g.Cfg().MustGet(ctx, "imInternalApi.serviceSecret").String(), + os.Getenv("IM_INTERNAL_API_SECRET"), + g.Cfg().MustGet(ctx, "imAdminApi.serviceSecret").String(), + os.Getenv("IM_ADMIN_API_SECRET"), + ) + return NewClient(Config{ + BaseURL: baseURL, + ServiceName: firstValue(g.Cfg().MustGet(ctx, "cultivateAdminApi.serviceName", "im-manager").String(), "im-manager"), + Secret: secret, + Timeout: timeout, + }) +} + +func NewClient(conf Config) *Client { + return &Client{conf: conf, http: &http.Client{Timeout: conf.Timeout}} +} + +func (c *Client) Get(ctx context.Context, scope Scope, path string, values url.Values, out any) error { + if values != nil { + if query := values.Encode(); query != "" { + path += "?" + query + } + } + return c.doJSON(ctx, http.MethodGet, scope, path, nil, out) +} + +func (c *Client) Post(ctx context.Context, scope Scope, path string, in any, out any) error { + return c.write(ctx, http.MethodPost, scope, path, in, out) +} + +func (c *Client) Put(ctx context.Context, scope Scope, path string, in any, out any) error { + return c.write(ctx, http.MethodPut, scope, path, in, out) +} + +func (c *Client) write(ctx context.Context, method string, scope Scope, path string, in any, out any) error { + body, err := json.Marshal(in) + if err != nil { + return gerror.Wrap(err, "序列化养成服务请求失败") + } + return c.doJSON(ctx, method, scope, path, body, out) +} + +func (c *Client) doJSON(ctx context.Context, method string, scope Scope, path string, body []byte, out any) error { + req, err := c.newRequest(ctx, method, scope, path, body) + if err != nil { + return err + } + resp, err := c.http.Do(req) + if err != nil { + g.Log().Warningf(ctx, "Cultivate admin request failed url:%s err:%+v", req.URL.String(), err) + return gerror.New("养成服务暂不可用") + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize)) + if err != nil { + return gerror.Wrap(err, "读取养成服务响应失败") + } + var envelope struct { + Code int `json:"code"` + Reason string `json:"reason"` + Message string `json:"message"` + RequestID string `json:"request_id"` + Data json.RawMessage `json:"data"` + } + if err = json.Unmarshal(raw, &envelope); err != nil { + g.Log().Warningf(ctx, "Cultivate admin invalid response status:%d requestId:%s", resp.StatusCode, scope.RequestID) + return gerror.New("养成服务返回异常") + } + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || envelope.Code != 0 { + return cultivateError(ctx, resp.StatusCode, envelope.Reason, envelope.Message, envelope.RequestID) + } + if out == nil || len(envelope.Data) == 0 || bytes.Equal(envelope.Data, []byte("null")) { + return nil + } + if err = unmarshalSnakeCase(envelope.Data, out); err != nil { + return gerror.Wrap(err, "解析养成服务响应失败") + } + return nil +} + +func (c *Client) newRequest(ctx context.Context, method string, scope Scope, path string, body []byte) (*http.Request, error) { + baseURL := strings.TrimRight(resolveValue(c.conf.BaseURL), "/") + secret := resolveValue(c.conf.Secret) + if baseURL == "" { + return nil, gerror.New("养成服务地址未配置") + } + if secret == "" { + return nil, gerror.New("养成服务内部鉴权密钥未配置") + } + if scope.TenantID <= 0 || strings.TrimSpace(scope.TenantCode) == "" || scope.OperatorID <= 0 { + return nil, gerror.New("养成服务管理范围不正确") + } + requestID := strings.TrimSpace(scope.RequestID) + if requestID == "" { + requestID = strings.TrimSpace(gctx.CtxId(ctx)) + } + nonce, err := randomNonce() + if err != nil { + return nil, err + } + if requestID == "" { + requestID = "manager-" + nonce + } + prefix := "/internal/admin/cultivate/tenants/" + url.PathEscape(strings.TrimSpace(scope.TenantCode)) + req, err := http.NewRequestWithContext(ctx, method, baseURL+prefix+path, bytes.NewReader(body)) + if err != nil { + return nil, gerror.Wrap(err, "创建养成服务请求失败") + } + timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10) + tenantID := strconv.FormatInt(scope.TenantID, 10) + operatorID := strconv.FormatInt(scope.OperatorID, 10) + bodyHash := sha256.Sum256(body) + canonical := strings.Join([]string{ + method, + req.URL.RequestURI(), + hex.EncodeToString(bodyHash[:]), + timestamp, + nonce, + tenantID, + operatorID, + scope.RoleKey, + requestID, + }, "\n") + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Service-Name", firstValue(c.conf.ServiceName, "im-manager")) + req.Header.Set("X-Timestamp", timestamp) + req.Header.Set("X-Nonce", nonce) + req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil))) + req.Header.Set("X-Tenant-ID", tenantID) + req.Header.Set("X-Operator-ID", operatorID) + req.Header.Set("X-Role-Key", scope.RoleKey) + req.Header.Set("X-Request-ID", requestID) + return req, nil +} + +func cultivateError(ctx context.Context, status int, reason, message, requestID string) error { + if status == http.StatusUnauthorized || status == http.StatusForbidden { + g.Log().Warningf(ctx, "Cultivate admin auth failed status:%d reason:%s requestId:%s", status, reason, requestID) + return gerror.New("养成服务内部鉴权失败") + } + if status >= http.StatusInternalServerError { + g.Log().Warningf(ctx, "Cultivate admin failed status:%d reason:%s requestId:%s", status, reason, requestID) + if status == http.StatusServiceUnavailable && message != "" { + return gerror.New(message) + } + return gerror.New("养成服务暂不可用") + } + if strings.TrimSpace(message) != "" { + return gerror.New(message) + } + if strings.TrimSpace(reason) != "" { + return gerror.New(reason) + } + return gerror.New("养成服务请求失败") +} + +func unmarshalSnakeCase(raw []byte, out any) error { + var value any + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + if err := decoder.Decode(&value); err != nil { + return err + } + converted, err := json.Marshal(convertKeys(value)) + if err != nil { + return err + } + return json.Unmarshal(converted, out) +} + +func convertKeys(value any) any { + switch typed := value.(type) { + case map[string]any: + result := make(map[string]any, len(typed)) + for key, item := range typed { + result[snakeToLowerCamel(key)] = convertKeys(item) + } + return result + case []any: + result := make([]any, len(typed)) + for index, item := range typed { + result[index] = convertKeys(item) + } + return result + default: + return value + } +} + +func snakeToLowerCamel(value string) string { + parts := strings.Split(value, "_") + if len(parts) == 1 { + return value + } + var builder strings.Builder + builder.WriteString(parts[0]) + for _, part := range parts[1:] { + if part == "" { + continue + } + runes := []rune(part) + builder.WriteRune(unicode.ToUpper(runes[0])) + builder.WriteString(string(runes[1:])) + } + return builder.String() +} + +func randomNonce() (string, error) { + var value [18]byte + if _, err := cryptorand.Read(value[:]); err != nil { + return "", gerror.Wrap(err, "生成养成服务请求随机数失败") + } + return hex.EncodeToString(value[:]), nil +} + +func firstValue(values ...string) string { + for _, value := range values { + if resolved := resolveValue(value); resolved != "" { + return resolved + } + } + return "" +} + +func resolveValue(value string) string { + value = strings.TrimSpace(value) + if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") { + return strings.TrimSpace(os.Getenv(strings.TrimSuffix(strings.TrimPrefix(value, "${"), "}"))) + } + if strings.HasPrefix(value, "$") && !strings.Contains(value, "://") { + return strings.TrimSpace(os.Getenv(strings.TrimPrefix(value, "$"))) + } + return value +} diff --git a/server/internal/library/cultivateadmin/client_test.go b/server/internal/library/cultivateadmin/client_test.go new file mode 100644 index 0000000..c6d9c34 --- /dev/null +++ b/server/internal/library/cultivateadmin/client_test.go @@ -0,0 +1,105 @@ +package cultivateadmin + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" +) + +func TestClientSignsScopeAndConvertsSnakeCase(t *testing.T) { + const secret = "test-secret" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if r.URL.RequestURI() != "/internal/admin/cultivate/tenants/default/dashboard?a=1&b=2" { + t.Fatalf("request URI = %q", r.URL.RequestURI()) + } + if r.Header.Get("X-Tenant-ID") != "1" || r.Header.Get("X-Operator-ID") != "14" { + t.Fatalf("unexpected scope headers") + } + hash := sha256.Sum256(body) + canonical := strings.Join([]string{ + r.Method, + r.URL.RequestURI(), + hex.EncodeToString(hash[:]), + r.Header.Get("X-Timestamp"), + r.Header.Get("X-Nonce"), + "1", + "14", + "imManager", + "req-test", + }, "\n") + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + if !hmac.Equal([]byte(r.Header.Get("X-Signature")), []byte(hex.EncodeToString(mac.Sum(nil)))) { + t.Fatal("signature mismatch") + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "code": 0, + "data": map[string]any{"plant_count": 7, "active_7d": 3}, + }) + })) + defer server.Close() + + client := NewClient(Config{BaseURL: server.URL, ServiceName: "im-manager", Secret: secret, Timeout: time.Second}) + client.http = server.Client() + var out struct { + PlantCount int64 `json:"plantCount"` + Active7d int64 `json:"active7d"` + } + values := make(url.Values) + values["b"] = []string{"2"} + values["a"] = []string{"1"} + err := client.Get(context.Background(), Scope{TenantID: 1, TenantCode: "default", OperatorID: 14, RoleKey: "imManager", RequestID: "req-test"}, "/dashboard", values, &out) + if err != nil { + t.Fatal(err) + } + if out.PlantCount != 7 || out.Active7d != 3 { + t.Fatalf("unexpected response: %+v", out) + } +} + +func TestClientReturnsBusinessMessage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusConflict) + _, _ = w.Write([]byte(`{"code":40901,"reason":"STATE_CONFLICT","message":"当前状态不允许操作"}`)) + })) + defer server.Close() + + client := NewClient(Config{BaseURL: server.URL, ServiceName: "im-manager", Secret: "secret", Timeout: time.Second}) + client.http = server.Client() + err := client.Post(context.Background(), Scope{TenantID: 1, TenantCode: "default", OperatorID: 14, RequestID: "req-test"}, "/content-reviews/feed_post/1/actions", map[string]any{"action": "approve"}, nil) + if err == nil || err.Error() != "当前状态不允许操作" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestClientGeneratesRequestIDWhenContextHasNone(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID := r.Header.Get("X-Request-ID") + if !strings.HasPrefix(requestID, "manager-") || len(requestID) <= len("manager-") { + t.Fatalf("unexpected request ID: %q", requestID) + } + _, _ = w.Write([]byte(`{"code":0,"data":null}`)) + })) + defer server.Close() + + client := NewClient(Config{BaseURL: server.URL, ServiceName: "im-manager", Secret: "secret", Timeout: time.Second}) + client.http = server.Client() + err := client.Get(context.Background(), Scope{TenantID: 1, TenantCode: "default", OperatorID: 14}, "/dashboard", nil, nil) + if err != nil { + t.Fatal(err) + } +} diff --git a/server/internal/logic/sys/cultivate_content.go b/server/internal/logic/sys/cultivate_content.go index 63b24d9..7f6daaa 100644 --- a/server/internal/logic/sys/cultivate_content.go +++ b/server/internal/logic/sys/cultivate_content.go @@ -2,25 +2,10 @@ package sys import ( "context" - "database/sql" - "errors" - "fmt" "net/url" - "strconv" "strings" - "time" - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" - - "hotgo/internal/consts" - "hotgo/internal/dao" - "hotgo/internal/library/contexts" - "hotgo/internal/library/imadmin" - "hotgo/internal/library/imtenant" - "hotgo/internal/model/entity" + "hotgo/internal/library/cultivateadmin" "hotgo/internal/model/input/sysin" "hotgo/internal/service" ) @@ -30,709 +15,167 @@ type sSysCultivateContent struct{} func NewSysCultivateContent() *sSysCultivateContent { return &sSysCultivateContent{} } func init() { service.RegisterSysCultivateContent(NewSysCultivateContent()) } -func (s *sSysCultivateContent) Dashboard(ctx context.Context, _ *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) { - tenantID := cultivateTenantID(ctx) - out := new(sysin.CultivateDashboardModel) - db := g.DB("cultivate") - queries := []struct { - target *int64 - sql string - args []any - }{ - {&out.PlantCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=?", []any{tenantID}}, - {&out.GrowingCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='growing'", []any{tenantID}}, - {&out.BloomCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='bloomed'", []any{tenantID}}, - {&out.HarvestedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='harvested'", []any{tenantID}}, - {&out.RemovedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='removed'", []any{tenantID}}, - {&out.Active7d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 6 DAY)", []any{tenantID}}, - {&out.Active30d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 29 DAY)", []any{tenantID}}, - {&out.PostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published'", []any{tenantID}}, - {&out.PublicPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='public'", []any{tenantID}}, - {&out.FriendsPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='friends'", []any{tenantID}}, - {&out.PrivatePostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='private'", []any{tenantID}}, - {&out.CommentCount, "SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='published'", []any{tenantID}}, - {&out.LikeCount, "SELECT COALESCE(SUM(like_count),0) FROM cv_feed_post WHERE tenant_id=?", []any{tenantID}}, - {&out.TakenDownCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='taken_down'", []any{tenantID}}, - {&out.PendingAuditCount, "SELECT (SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='reviewing')+(SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='reviewing')", []any{tenantID, tenantID}}, - {&out.ApprovedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='published'", []any{tenantID}}, - {&out.RejectedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='rejected'", []any{tenantID}}, - {&out.PendingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='submitted'", []any{tenantID}}, - {&out.ProcessingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='processing'", []any{tenantID}}, - {&out.ProcessedReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status IN ('resolved','rejected')", []any{tenantID}}, +func (s *sSysCultivateContent) Dashboard(ctx context.Context, in *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err } - for _, q := range queries { - value, err := db.GetValue(ctx, q.sql, q.args...) - if err != nil { - return nil, gerror.Wrap(err, "获取养成统计失败") - } - *q.target = value.Int64() + scope, err := cultivateAdminScope(ctx) + if err != nil { + return nil, err } - return out, nil + result := new(sysin.CultivateDashboardModel) + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/dashboard", nil, result); err != nil { + return nil, err + } + return result, nil } func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) { if err = in.Filter(ctx); err != nil { return nil, 0, err } - tenantID := cultivateTenantID(ctx) - if in.ObjectType == "feed_comment" { - columns := dao.FeedComment.Columns() - mod := dao.FeedComment.Ctx(ctx).Where(columns.TenantId, tenantID) - if in.AuthorId != "" { - mod = mod.Where(columns.AuthorId, in.AuthorId) - } - if in.Status != "" { - mod = mod.Where(columns.Status, in.Status) - } - if in.Keyword != "" { - mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%") - } - if len(in.CreatedAt) == 2 { - mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) - } - var rows []*entity.FeedComment - err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false) - if err != nil { - return nil, 0, gerror.Wrap(err, "获取评论审核列表失败") - } - list = make([]*sysin.CultivateContentListModel, 0, len(rows)) - for _, row := range rows { - list = append(list, &sysin.CultivateContentListModel{Id: strconv.FormatUint(row.Id, 10), ObjectType: "feed_comment", PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}) - } - return - } - columns := dao.FeedPost.Columns() - mod := dao.FeedPost.Ctx(ctx).Where(columns.TenantId, tenantID) - if in.AuthorId != "" { - mod = mod.Where(columns.AuthorId, in.AuthorId) - } - if in.Status != "" { - mod = mod.Where(columns.Status, in.Status) - } - if in.Keyword != "" { - mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%") - } - if len(in.CreatedAt) == 2 { - mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) - } - var rows []*entity.FeedPost - err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false) + scope, err := cultivateAdminScope(ctx) if err != nil { - return nil, 0, gerror.Wrap(err, "获取动态审核列表失败") + return nil, 0, err } - list = make([]*sysin.CultivateContentListModel, 0, len(rows)) - for _, row := range rows { - list = append(list, &sysin.CultivateContentListModel{Id: strconv.FormatUint(row.Id, 10), ObjectType: "feed_post", AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, FlowerCode: row.FlowerCode, Visibility: row.Visibility, Status: row.Status, AuditStatus: row.AuditStatus, RejectReason: row.RejectReasonCode, LikeCount: row.LikeCount, CommentCount: row.CommentCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}) + normalizeCultivatePage(&in.PageReq) + values := cultivatePageValues(in.Page, in.PerPage) + cultivateSetQuery(values, "object_type", in.ObjectType) + cultivateSetQuery(values, "author_id", in.AuthorId) + cultivateSetQuery(values, "status", in.Status) + cultivateSetQuery(values, "keyword", in.Keyword) + setTimeRange(values, in.CreatedAt) + var page cultivateadmin.Page[*sysin.CultivateContentListModel] + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/content-reviews", values, &page); err != nil { + return nil, 0, err } - return + if page.Items == nil { + page.Items = []*sysin.CultivateContentListModel{} + } + return page.Items, page.Total, nil } func (s *sSysCultivateContent) View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) { if err := in.Filter(ctx); err != nil { return nil, err } - tenantID := cultivateTenantID(ctx) - base := &sysin.CultivateContentViewModel{Media: []*sysin.CultivateMediaModel{}, Audits: []*sysin.CultivateAuditModel{}, RelatedReports: []*sysin.CultivateReportListModel{}} - if in.ObjectType == "feed_comment" { - c := dao.FeedComment.Columns() - var row entity.FeedComment - if err := dao.FeedComment.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, err - } - if row.Id == 0 { - return nil, gerror.New("评论不存在") - } - base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt} - } else { - c := dao.FeedPost.Columns() - var row entity.FeedPost - if err := dao.FeedPost.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, err - } - if row.Id == 0 { - return nil, gerror.New("动态不存在") - } - base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, FlowerCode: row.FlowerCode, Visibility: row.Visibility, Status: row.Status, AuditStatus: row.AuditStatus, RejectReason: row.RejectReasonCode, LikeCount: row.LikeCount, CommentCount: row.CommentCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt} - mc := dao.FeedMedia.Columns() - var media []*entity.FeedMedia - if err := dao.FeedMedia.Ctx(ctx).Where(g.Map{mc.TenantId: tenantID, mc.PostId: in.ObjectId}).OrderAsc(mc.SortNo).Scan(&media); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, err - } - for _, m := range media { - base.Media = append(base.Media, &sysin.CultivateMediaModel{Id: strconv.FormatUint(m.Id, 10), FileId: m.FileId, ThumbnailFileId: m.ThumbnailFileId, MediaType: m.MediaType, AuditStatus: m.AuditStatus, SortNo: m.SortNo}) - } - } - ac := dao.ContentAuditLog.Columns() - var audits []*entity.ContentAuditLog - if err := dao.ContentAuditLog.Ctx(ctx).Where(g.Map{ac.TenantId: tenantID, ac.ObjectType: in.ObjectType, ac.ObjectId: in.ObjectId}).OrderDesc(ac.Id).Scan(&audits); err != nil && !errors.Is(err, sql.ErrNoRows) { + scope, err := cultivateAdminScope(ctx) + if err != nil { return nil, err } - for _, a := range audits { - base.Audits = append(base.Audits, &sysin.CultivateAuditModel{Id: strconv.FormatUint(a.Id, 10), AuditSource: a.AuditSource, StatusBefore: a.StatusBefore, StatusAfter: a.StatusAfter, ResultCode: a.ResultCode, RiskLabels: jsonValue(a.RiskLabels), OperatorId: optionalUint(a.OperatorId), OccurredAt: a.OccurredAt}) + result := new(sysin.CultivateContentViewModel) + path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id) + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, path, nil, result); err != nil { + return nil, err } - rc := dao.FeedReport.Columns() - var reports []*entity.FeedReport - if err := dao.FeedReport.Ctx(ctx).Where(g.Map{rc.TenantId: tenantID, rc.ObjectType: in.ObjectType, rc.ObjectId: in.Id}).OrderDesc(rc.Id).Scan(&reports); err == nil { - for _, r := range reports { - base.RelatedReports = append(base.RelatedReports, reportModel(r)) - } - } - profiles, _ := cultivateUserProfiles(ctx, tenantID, []string{base.AuthorId}) - if p := profiles[base.AuthorId]; p != nil { - base.AuthorNickname = p.Nickname - base.AuthorAvatarFileId = p.AvatarFileId - } - return base, nil + return result, nil } func (s *sSysCultivateContent) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error { if err := in.Filter(ctx); err != nil { return err } - tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx) - var authorID uint64 - err := dao.FeedPost.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { - if in.ObjectType == "feed_comment" { - var row entity.FeedComment - c := dao.FeedComment.Columns() - if e := tx.Model(dao.FeedComment.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil { - return e - } - authorID = row.AuthorId - return s.reviewComment(txCtx, tx, tenantID, operatorID, in) - } - var row entity.FeedPost - c := dao.FeedPost.Columns() - if e := tx.Model(dao.FeedPost.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil { - return e - } - authorID = row.AuthorId - return s.reviewPost(txCtx, tx, tenantID, operatorID, in) - }) + scope, err := cultivateAdminScope(ctx) if err != nil { return err } - if authorID > 0 { - publishCultivateNotice(ctx, tenantID, authorID, fmt.Sprintf("admin.content.%s.%s.%s", in.ObjectType, in.Id, in.Action), "cultivate.content.reviewed", "内容审核结果", cultivateReviewSummary(in.Action), map[string]any{"type": in.ObjectType, "object_type": in.ObjectType, "object_id": in.Id}) - } - return nil + path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id) + "/actions" + return cultivateadmin.DefaultClient(ctx).Post(ctx, scope, path, map[string]any{"action": in.Action, "reason": strings.TrimSpace(in.Reason)}, nil) } func (s *sSysCultivateContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) { if err := in.Filter(ctx); err != nil { return nil, err } - result := &sysin.CultivateContentBatchReviewModel{Failures: make(map[string]string)} - for _, id := range in.Ids { - item := &sysin.CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason} - if err := item.Filter(ctx); err != nil { - result.FailedCount++ - result.Failures[id] = err.Error() - continue - } - if err := s.Review(ctx, item); err != nil { - result.FailedCount++ - result.Failures[id] = err.Error() - continue - } - result.SuccessCount++ + scope, err := cultivateAdminScope(ctx) + if err != nil { + return nil, err + } + result := new(sysin.CultivateContentBatchReviewModel) + err = cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/content-reviews/batch-actions", map[string]any{ + "object_type": in.ObjectType, "ids": in.Ids, "action": in.Action, "reason": strings.TrimSpace(in.Reason), + }, result) + if err != nil { + return nil, err } return result, nil } -func (s *sSysCultivateContent) reviewPost(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error { - columns := dao.FeedPost.Columns() - var before entity.FeedPost - err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - if before.Id == 0 { - return gerror.New("动态不存在") - } - now := time.Now().UTC() - data := g.Map{columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")} - afterStatus := before.Status - switch in.Action { - case "approve": - if before.Status != "reviewing" && before.Status != "rejected" { - return gerror.New("仅待审核或已拒绝动态可审核通过") - } - afterStatus = "published" - data[columns.Status] = afterStatus - data[columns.AuditStatus] = "approved" - data[columns.RejectReasonCode] = "" - if before.PublishedAt == nil { - data[columns.PublishedAt] = now - } - case "reject": - if before.Status != "reviewing" { - return gerror.New("仅待审核动态可拒绝") - } - afterStatus = "rejected" - data[columns.Status] = afterStatus - data[columns.AuditStatus] = "rejected" - data[columns.RejectReasonCode] = in.Reason - case "take_down": - if before.Status != "published" { - return gerror.New("仅已发布动态可下架") - } - afterStatus = "taken_down" - data[columns.Status] = afterStatus - data[columns.TakenDownAt] = now - data[columns.RejectReasonCode] = in.Reason - case "restore": - if before.Status != "taken_down" { - return gerror.New("仅已下架动态可恢复") - } - afterStatus = "published" - data[columns.Status] = afterStatus - data[columns.TakenDownAt] = nil - data[columns.RejectReasonCode] = "" - } - if _, err = tx.Model(dao.FeedPost.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { - return gerror.Wrap(err, "更新动态审核状态失败") - } - if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_post", before.Id, before.Status, afterStatus, in.Action, now); err != nil { - return err - } - return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_post", in.Id, before, data, in.Reason, now) -} - -func (s *sSysCultivateContent) reviewComment(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error { - columns := dao.FeedComment.Columns() - var before entity.FeedComment - err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - if before.Id == 0 { - return gerror.New("评论不存在") - } - now := time.Now().UTC() - data := g.Map{columns.UpdatedAt: now} - afterStatus := before.Status - counterDelta := 0 - switch in.Action { - case "approve": - if before.Status != "reviewing" && before.Status != "rejected" { - return gerror.New("仅待审核或已拒绝评论可审核通过") - } - afterStatus = "published" - data[columns.Status] = afterStatus - data[columns.AuditStatus] = "approved" - data[columns.PublishedAt] = now - counterDelta = 1 - case "reject": - if before.Status != "reviewing" { - return gerror.New("仅待审核评论可拒绝") - } - afterStatus = "rejected" - data[columns.Status] = afterStatus - data[columns.AuditStatus] = "rejected" - data[columns.DeleteReason] = in.Reason - case "take_down": - if before.Status != "published" { - return gerror.New("仅已发布评论可下架") - } - afterStatus = "taken_down" - data[columns.Status] = afterStatus - data[columns.DeleteReason] = in.Reason - counterDelta = -1 - case "restore": - if before.Status != "taken_down" { - return gerror.New("仅已下架评论可恢复") - } - afterStatus = "published" - data[columns.Status] = afterStatus - data[columns.DeleteReason] = "" - counterDelta = 1 - } - if _, err = tx.Model(dao.FeedComment.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { - return gerror.Wrap(err, "更新评论审核状态失败") - } - if counterDelta != 0 { - postColumns := dao.FeedPost.Columns() - expression := "`comment_count` + 1" - if counterDelta < 0 { - expression = "GREATEST(`comment_count` - 1, 0)" - } - if _, err = tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw(expression), postColumns.UpdatedAt: now}).Update(); err != nil { - return gerror.Wrap(err, "更新动态评论数失败") - } - } - if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_comment", before.Id, before.Status, afterStatus, in.Action, now); err != nil { - return err - } - return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_comment", in.Id, before, data, in.Reason, now) -} - -func writeContentAudit(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, objectType string, objectID uint64, before, after, result string, now time.Time) error { - id, err := imtenant.NextSnowflakeID(ctx) - if err != nil { - return err - } - columns := dao.ContentAuditLog.Columns() - requestID := fmt.Sprintf("manual-%s-%d-%d", objectType, objectID, id) - _, err = tx.Model(dao.ContentAuditLog.Table()).Data(g.Map{columns.Id: id, columns.TenantId: tenantID, columns.ObjectType: objectType, columns.ObjectId: objectID, columns.AuditRequestId: requestID, columns.AuditSource: "manual", columns.StatusBefore: before, columns.StatusAfter: after, columns.ResultCode: result, columns.RiskLabels: "[]", columns.OperatorId: operatorID, columns.OccurredAt: now, columns.CreatedAt: now}).Insert() - return err -} - func (s *sSysCultivateContent) ReportList(ctx context.Context, in *sysin.CultivateReportListInp) (list []*sysin.CultivateReportListModel, totalCount int, err error) { if err = in.Filter(ctx); err != nil { return nil, 0, err } - columns := dao.FeedReport.Columns() - mod := dao.FeedReport.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx)) - if in.ObjectType != "" { - mod = mod.Where(columns.ObjectType, in.ObjectType) + scope, err := cultivateAdminScope(ctx) + if err != nil { + return nil, 0, err } - if in.Status != "" { - mod = mod.Where(columns.Status, in.Status) + normalizeCultivatePage(&in.PageReq) + values := cultivatePageValues(in.Page, in.PerPage) + cultivateSetQuery(values, "object_type", in.ObjectType) + cultivateSetQuery(values, "status", in.Status) + cultivateSetQuery(values, "reporter_id", in.ReporterId) + setTimeRange(values, in.CreatedAt) + var page cultivateadmin.Page[*sysin.CultivateReportListModel] + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/reports", values, &page); err != nil { + return nil, 0, err } - if in.ReporterId != "" { - mod = mod.Where(columns.ReporterId, in.ReporterId) + if page.Items == nil { + page.Items = []*sysin.CultivateReportListModel{} } - if len(in.CreatedAt) == 2 { - mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) - } - var rows []*entity.FeedReport - if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil { - return nil, 0, gerror.Wrap(err, "获取举报列表失败") - } - list = make([]*sysin.CultivateReportListModel, 0, len(rows)) - for _, row := range rows { - list = append(list, reportModel(row)) - } - return -} - -func reportModel(row *entity.FeedReport) *sysin.CultivateReportListModel { - return &sysin.CultivateReportListModel{Id: strconv.FormatUint(row.Id, 10), ReporterId: strconv.FormatUint(row.ReporterId, 10), ReportedUserId: strconv.FormatUint(row.ReportedUserId, 10), ObjectType: row.ObjectType, ObjectId: row.ObjectId, ReasonCode: row.ReasonCode, Description: row.Description, EvidenceFileIds: row.EvidenceFileIds, SourcePage: row.SourcePage, Status: row.Status, HandlerId: optionalUint(row.HandlerId), HandleResult: row.HandleResult, HandledAt: row.HandledAt, CreatedAt: row.CreatedAt} + return page.Items, page.Total, nil } func (s *sSysCultivateContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) { if err := in.Filter(ctx); err != nil { return nil, err } - tenantID := cultivateTenantID(ctx) - c := dao.FeedReport.Columns() - var row entity.FeedReport - if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.Id: in.ReportId, c.TenantId: tenantID}).Scan(&row); err != nil { + scope, err := cultivateAdminScope(ctx) + if err != nil { return nil, err } - if row.Id == 0 { - return nil, gerror.New("举报记录不存在") + result := new(sysin.CultivateReportViewModel) + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/reports/"+url.PathEscape(in.Id), nil, result); err != nil { + return nil, err } - out := &sysin.CultivateReportViewModel{CultivateReportListModel: *reportModel(&row), History: []*sysin.CultivateReportListModel{}} - profiles, _ := cultivateUserProfiles(ctx, tenantID, []string{out.ReporterId, out.ReportedUserId}) - if p := profiles[out.ReporterId]; p != nil { - out.ReporterNickname = p.Nickname - } - if p := profiles[out.ReportedUserId]; p != nil { - out.ReportedUserNickname = p.Nickname - } - var history []*entity.FeedReport - if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.TenantId: tenantID, c.ReportedUserId: row.ReportedUserId}).OrderDesc(c.Id).Limit(20).Scan(&history); err == nil { - for _, item := range history { - out.History = append(out.History, reportModel(item)) - } - } - if row.ObjectType == "feed_post" || row.ObjectType == "feed_comment" { - view, err := s.View(ctx, &sysin.CultivateContentViewInp{ObjectType: row.ObjectType, Id: row.ObjectId, ObjectId: mustUint(row.ObjectId)}) - if err == nil { - out.TargetContent = &view.CultivateContentListModel - } - } - return out, nil + return result, nil } func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error { if err := in.Filter(ctx); err != nil { return err } - tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx) - columns := dao.FeedReport.Columns() - var report entity.FeedReport - if err := dao.FeedReport.Ctx(ctx).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).Scan(&report); err != nil { - return err - } - if report.Id == 0 { - return gerror.New("举报记录不存在") - } - if report.Status == "resolved" || report.Status == "rejected" { - return gerror.New("举报已办结,不能重复处理") - } - if in.ContentAction == "take_down" { - if err := validateReportedContentCanTakeDown(ctx, tenantID, &report); err != nil { - return err - } - } - if in.UserAction != "none" && report.ReportedUserId == 0 { - return gerror.New("被举报用户不存在,无法执行用户处置") - } - if in.UserAction != "none" { - if err := governCultivateUser(ctx, tenantID, report.ReportedUserId, in.UserAction, in.Reason); err != nil { - return gerror.Wrap(err, "用户联动处置失败") - } - } - err := dao.FeedReport.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { - columns := dao.FeedReport.Columns() - var before entity.FeedReport - err := tx.Model(dao.FeedReport.Table()).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - if before.Id == 0 { - return gerror.New("举报记录不存在") - } - if before.Status == "resolved" || before.Status == "rejected" { - return gerror.New("举报已办结,不能重复处理") - } - now := time.Now().UTC() - status := in.Action - if status == "resolve" { - status = "resolved" - } - data := g.Map{columns.Status: status, columns.HandlerId: operatorID, columns.HandleResult: in.HandleResult, columns.UpdatedAt: now} - if status == "resolved" || status == "rejected" { - data[columns.HandledAt] = now - } - if in.ContentAction == "take_down" { - if err = takeDownReportedContent(txCtx, tx, tenantID, operatorID, &before, in.Reason, now); err != nil { - return err - } - } - if _, err = tx.Model(dao.FeedReport.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { - return gerror.Wrap(err, "更新举报处理状态失败") - } - return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "report."+in.Action, "feed_report", in.Id, before, data, in.HandleResult, now) - }) + scope, err := cultivateAdminScope(ctx) if err != nil { return err } - if in.Action == "resolve" || in.Action == "reject" { - summary := "你的举报已处理,感谢你帮助维护社区环境" - if in.Action == "reject" { - summary = "你的举报已完成核查,暂未发现违规" - } - publishCultivateNotice(ctx, tenantID, report.ReporterId, "admin.report."+in.Id, "cultivate.report.handled", "举报处理结果", summary, map[string]any{"type": "report", "report_id": in.Id}) - } - if in.ContentAction != "none" || in.UserAction != "none" { - publishCultivateNotice(ctx, tenantID, report.ReportedUserId, "admin.report.target."+in.Id, "cultivate.governance.applied", "社区治理通知", "你的内容或账号已根据社区规则进行处置", map[string]any{"type": "report", "report_id": in.Id}) - } - return nil + return cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/reports/"+url.PathEscape(in.Id)+"/actions", map[string]any{ + "action": in.Action, "handle_result": in.HandleResult, "content_action": in.ContentAction, + "user_action": in.UserAction, "reason": in.Reason, + }, nil) } func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) { - columns := dao.AdminOperationLog.Columns() - mod := dao.AdminOperationLog.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx)) - if in.OperatorId != "" { - mod = mod.Where(columns.OperatorId, in.OperatorId) + if err = in.Filter(ctx); err != nil { + return nil, 0, err } - if in.OperationType != "" { - mod = mod.WhereLike(columns.OperationType, "%"+strings.TrimSpace(in.OperationType)+"%") - } - if in.TargetType != "" { - mod = mod.Where(columns.TargetType, in.TargetType) - } - if in.TargetId != "" { - mod = mod.Where(columns.TargetId, strings.TrimSpace(in.TargetId)) - } - if in.RequestId != "" { - mod = mod.Where(columns.RequestId, strings.TrimSpace(in.RequestId)) - } - if len(in.CreatedAt) == 2 { - mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) - } - var rows []*entity.AdminOperationLog - if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil { - return nil, 0, gerror.Wrap(err, "获取操作日志失败") - } - list = make([]*sysin.CultivateOperationLogListModel, 0, len(rows)) - for _, row := range rows { - list = append(list, &sysin.CultivateOperationLogListModel{Id: strconv.FormatUint(row.Id, 10), OperatorId: strconv.FormatUint(row.OperatorId, 10), OperationType: row.OperationType, TargetType: row.TargetType, TargetId: row.TargetId, RequestId: row.RequestId, BeforeSnapshot: jsonValue(row.BeforeSnapshot), AfterSnapshot: jsonValue(row.AfterSnapshot), Reason: row.Reason, CreatedAt: row.CreatedAt}) - } - return -} - -func optionalUint(value uint64) string { - if value == 0 { - return "" - } - return strconv.FormatUint(value, 10) -} -func jsonValue(value *gjson.Json) any { - if value == nil { - return nil - } - return value -} - -type cultivateProfile struct { - UserId int64 `json:"user_id"` - Nickname string `json:"nickname"` - AvatarFileId string `json:"avatar_file_id"` -} - -func cultivateUserProfiles(ctx context.Context, tenantID int64, userIDs []string) (map[string]*cultivateProfile, error) { - result := make(map[string]*cultivateProfile) - ids := make([]int64, 0, len(userIDs)) - for _, value := range userIDs { - id, err := strconv.ParseInt(value, 10, 64) - if err == nil && id > 0 { - ids = append(ids, id) - } - } - if len(ids) == 0 { - return result, nil - } - tenant, err := resolveImAdminTenant(ctx, tenantID) + scope, err := cultivateAdminScope(ctx) if err != nil { - return nil, err + return nil, 0, err } - var out struct { - Items []*cultivateProfile `json:"items"` + normalizeCultivatePage(&in.PageReq) + values := cultivatePageValues(in.Page, in.PerPage) + cultivateSetQuery(values, "operator_id", in.OperatorId) + cultivateSetQuery(values, "operation_type", in.OperationType) + cultivateSetQuery(values, "target_type", in.TargetType) + cultivateSetQuery(values, "target_id", in.TargetId) + cultivateSetQuery(values, "request_id", in.RequestId) + setTimeRange(values, in.CreatedAt) + var page cultivateadmin.Page[*sysin.CultivateOperationLogListModel] + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/operation-logs", values, &page); err != nil { + return nil, 0, err } - path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/users/batch" - if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_ids": ids}, &out); err != nil { - return nil, err + if page.Items == nil { + page.Items = []*sysin.CultivateOperationLogListModel{} } - for _, item := range out.Items { - result[strconv.FormatInt(item.UserId, 10)] = item - } - return result, nil -} - -func publishCultivateNotice(ctx context.Context, tenantID int64, targetUserID uint64, eventID, eventType, title, summary string, action map[string]any) { - if targetUserID == 0 { - return - } - tenant, err := resolveImAdminTenant(ctx, tenantID) - if err == nil { - path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/events/publish" - payload := map[string]any{"event_id": eventID, "event_type": eventType, "actor_id": contexts.GetUserId(ctx), "target_user_id": targetUserID, "title": title, "summary": summary, "action": action} - err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil) - } - if err != nil { - g.Log().Warningf(ctx, "养成通知投递失败: %v", err) - } -} - -func cultivateReviewSummary(action string) string { - switch action { - case "approve": - return "你的内容已审核通过" - case "reject": - return "你的内容未通过审核,请修改后重试" - case "take_down": - return "你的内容已根据社区规则下架" - case "restore": - return "你的内容已恢复展示" - default: - return "你的内容审核状态已更新" - } -} - -func mustUint(value string) uint64 { - id, _ := strconv.ParseUint(value, 10, 64) - return id -} - -func governCultivateUser(ctx context.Context, tenantID int64, userID uint64, action, reason string) error { - tenant, err := resolveImAdminTenant(ctx, tenantID) - if err != nil { - return err - } - eventType := consts.ImAdminCommandUserFreeze - if action == "ban" { - eventType = consts.ImAdminCommandUserBan - } - path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users/" + strconv.FormatUint(userID, 10) + "/action" - payload := map[string]any{"event_type": eventType, "actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10), "reason": reason} - return imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil) -} - -func takeDownReportedContent(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, report *entity.FeedReport, reason string, now time.Time) error { - id := mustUint(report.ObjectId) - if id == 0 { - return gerror.New("举报对象ID不正确") - } - if report.ObjectType == "feed_post" { - columns := dao.FeedPost.Columns() - var before entity.FeedPost - if err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil { - return err - } - if before.Id == 0 { - return gerror.New("被举报动态不存在") - } - if before.Status != "published" { - return gerror.New("仅已发布动态可联动下架") - } - data := g.Map{columns.Status: "taken_down", columns.TakenDownAt: now, columns.RejectReasonCode: reason, columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")} - if _, err := tx.Model(dao.FeedPost.Table()).Where(columns.Id, id).Data(data).Update(); err != nil { - return err - } - if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil { - return err - } - return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now) - } - if report.ObjectType == "feed_comment" { - columns := dao.FeedComment.Columns() - var before entity.FeedComment - if err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil { - return err - } - if before.Id == 0 { - return gerror.New("被举报评论不存在") - } - if before.Status != "published" { - return gerror.New("仅已发布评论可联动下架") - } - data := g.Map{columns.Status: "taken_down", columns.DeleteReason: reason, columns.UpdatedAt: now} - if _, err := tx.Model(dao.FeedComment.Table()).Where(columns.Id, id).Data(data).Update(); err != nil { - return err - } - postColumns := dao.FeedPost.Columns() - if _, err := tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw("GREATEST(`comment_count` - 1, 0)"), postColumns.UpdatedAt: now}).Update(); err != nil { - return err - } - if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil { - return err - } - return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now) - } - return gerror.New("该举报对象不支持内容联动下架") -} - -func validateReportedContentCanTakeDown(ctx context.Context, tenantID int64, report *entity.FeedReport) error { - id := mustUint(report.ObjectId) - if id == 0 { - return gerror.New("举报对象ID不正确") - } - switch report.ObjectType { - case "feed_post": - columns := dao.FeedPost.Columns() - value, err := dao.FeedPost.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status) - if err != nil { - return err - } - if value.String() != "published" { - return gerror.New("仅已发布动态可联动下架") - } - case "feed_comment": - columns := dao.FeedComment.Columns() - value, err := dao.FeedComment.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status) - if err != nil { - return err - } - if value.String() != "published" { - return gerror.New("仅已发布评论可联动下架") - } - default: - return gerror.New("该举报对象不支持内容联动下架") - } - return nil + return page.Items, page.Total, nil } diff --git a/server/internal/logic/sys/cultivate_flower.go b/server/internal/logic/sys/cultivate_flower.go index e25f369..9aad44b 100644 --- a/server/internal/logic/sys/cultivate_flower.go +++ b/server/internal/logic/sys/cultivate_flower.go @@ -3,24 +3,20 @@ package sys import ( "context" "encoding/json" - "fmt" "net/url" + "strconv" "strings" "time" - "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gtime" - "hotgo/internal/dao" "hotgo/internal/library/contexts" - "hotgo/internal/library/imadmin" - "hotgo/internal/library/imtenant" - "hotgo/internal/library/location" - "hotgo/internal/model/entity" + "hotgo/internal/library/cultivateadmin" + "hotgo/internal/model/input/form" "hotgo/internal/model/input/sysin" "hotgo/internal/service" ) @@ -30,6 +26,39 @@ type sSysCultivateFlower struct{} func NewSysCultivateFlower() *sSysCultivateFlower { return &sSysCultivateFlower{} } func init() { service.RegisterSysCultivateFlower(NewSysCultivateFlower()) } +type cultivateFlowerDTO struct { + ID string `json:"id"` + FlowerCode string `json:"flowerCode"` + FlowerName string `json:"flowerName"` + FlowerDesc string `json:"flowerDesc"` + IconFileID string `json:"iconFileId"` + Status string `json:"status"` + ConfigVersion string `json:"configVersion"` + DailyWaterLimit uint `json:"dailyWaterLimit"` + DailyFertilizeLimit uint `json:"dailyFertilizeLimit"` + ShareConfig any `json:"shareConfig"` + SortNo int `json:"sortNo"` + StageCount int `json:"stageCount"` + StageRules []*cultivateStageRuleDTO `json:"stageRules"` + CreatedAt *gtime.Time `json:"createdAt"` + UpdatedAt *gtime.Time `json:"updatedAt"` +} + +type cultivateStageRuleDTO struct { + ID string `json:"id"` + Stage string `json:"stage"` + NextStage string `json:"nextStage"` + RequiredPoints uint `json:"requiredPoints"` + WaterPoints uint `json:"waterPoints"` + FertilizePoints uint `json:"fertilizePoints"` + AllowedActions []string `json:"allowedActions"` + ImageFileID string `json:"imageFileId"` + AnimationFileID string `json:"animationFileId"` + ProgressTextTemplate string `json:"progressTextTemplate"` + CompletionText string `json:"completionText"` + SortNo int `json:"sortNo"` +} + func cultivateTenantID(ctx context.Context) int64 { id := contexts.GetTenantId(ctx) if id <= 0 { @@ -38,222 +67,212 @@ func cultivateTenantID(ctx context.Context) int64 { return id } -func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) { - c := dao.FlowerCatalog.Columns() - mod := dao.FlowerCatalog.Ctx(ctx).Fields("cv_flower_catalog.*, COUNT(cv_flower_stage_rule.id) AS stage_count").LeftJoin(dao.FlowerStageRule.Table(), "cv_flower_stage_rule.flower_catalog_id=cv_flower_catalog.id AND cv_flower_stage_rule.tenant_id=cv_flower_catalog.tenant_id").Where("cv_flower_catalog."+c.TenantId, cultivateTenantID(ctx)).Group("cv_flower_catalog." + c.Id) - if in.FlowerCode != "" { - mod = mod.WhereLike("cv_flower_catalog."+c.FlowerCode, "%"+strings.TrimSpace(in.FlowerCode)+"%") - } - if in.FlowerName != "" { - mod = mod.WhereLike("cv_flower_catalog."+c.FlowerName, "%"+strings.TrimSpace(in.FlowerName)+"%") - } - if in.Status != "" { - mod = mod.Where("cv_flower_catalog."+c.Status, in.Status) - } - if in.ConfigVersion != "" { - mod = mod.Where("cv_flower_catalog."+c.ConfigVersion, in.ConfigVersion) - } - if len(in.CreatedAt) == 2 { - mod = mod.WhereBetween("cv_flower_catalog."+c.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) - } - mod = mod.Page(in.Page, in.PerPage).OrderAsc("cv_flower_catalog." + c.SortNo).OrderDesc("cv_flower_catalog." + c.Id) - if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { - err = gerror.Wrap(err, "获取花种列表失败") - } - return -} - -func (s *sSysCultivateFlower) View(ctx context.Context, in *sysin.CultivateFlowerViewInp) (res *sysin.CultivateFlowerViewModel, err error) { - c := dao.FlowerCatalog.Columns() - var catalog entity.FlowerCatalog - if err = dao.FlowerCatalog.Ctx(ctx).Where(g.Map{c.Id: in.Id, c.TenantId: cultivateTenantID(ctx)}).Scan(&catalog); err != nil { - return - } - if catalog.Id == 0 { - return nil, gerror.New("花种不存在") - } - res = &sysin.CultivateFlowerViewModel{CultivateFlowerEditInp: sysin.CultivateFlowerEditInp{Id: catalog.Id, FlowerCode: catalog.FlowerCode, FlowerName: catalog.FlowerName, FlowerDesc: catalog.FlowerDesc, IconFileId: catalog.IconFileId, Status: catalog.Status, DailyWaterLimit: catalog.DailyWaterLimit, DailyFertilizeLimit: catalog.DailyFertilizeLimit, ShareConfig: catalog.ShareConfig, SortNo: catalog.SortNo}, ConfigVersion: catalog.ConfigVersion, CreatedAt: catalog.CreatedAt, UpdatedAt: catalog.UpdatedAt} - rc := dao.FlowerStageRule.Columns() - var rules []*entity.FlowerStageRule - if err = dao.FlowerStageRule.Ctx(ctx).Where(g.Map{rc.TenantId: cultivateTenantID(ctx), rc.FlowerCatalogId: catalog.Id}).OrderAsc(rc.SortNo).OrderAsc(rc.Id).Scan(&rules); err != nil { - return - } - for _, rule := range rules { - actions := []string{} - if rule.AllowedActions != nil { - _ = json.Unmarshal(rule.AllowedActions.MustToJson(), &actions) - } - res.StageRules = append(res.StageRules, &sysin.CultivateStageRuleEditInp{Id: rule.Id, Stage: rule.Stage, NextStage: rule.NextStage, RequiredPoints: rule.RequiredPoints, WaterPoints: rule.WaterPoints, FertilizePoints: rule.FertilizePoints, AllowedActions: actions, ImageFileId: rule.ImageFileId, AnimationFileId: rule.AnimationFileId, ProgressTextTemplate: rule.ProgressTextTemplate, CompletionText: rule.CompletionText, SortNo: rule.SortNo}) - } - return -} - -func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowerEditInp) (err error) { - if err = in.Filter(ctx); err != nil { - return err - } - tenantID := cultivateTenantID(ctx) - if err = validateCultivateAssets(ctx, tenantID, in); err != nil { - return err +func cultivateAdminScope(ctx context.Context) (cultivateadmin.Scope, error) { + tenant, err := resolveImAdminTenant(ctx, cultivateTenantID(ctx)) + if err != nil { + return cultivateadmin.Scope{}, err } operatorID := contexts.GetUserId(ctx) - now := time.Now().UTC() - version := "cat_" + now.Format("20060102_150405") - return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { - c := dao.FlowerCatalog.Columns() - var before entity.FlowerCatalog - if in.Id > 0 { - if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil { - return e - } - if before.Id == 0 { - return gerror.New("花种不存在") - } - if before.FlowerCode != in.FlowerCode { - return gerror.New("花种编码创建后不可修改") - } - } - shareJSON := "{}" - if in.ShareConfig != nil { - shareJSON = in.ShareConfig.MustToJsonString() - } - data := g.Map{c.FlowerName: in.FlowerName, c.FlowerDesc: in.FlowerDesc, c.IconFileId: in.IconFileId, c.Status: in.Status, c.ConfigVersion: version, c.DailyWaterLimit: in.DailyWaterLimit, c.DailyFertilizeLimit: in.DailyFertilizeLimit, c.ShareConfig: shareJSON, c.SortNo: in.SortNo, c.UpdatedAt: now} - if in.Status == "online" { - data[c.OnlineAt] = now - data[c.OfflineAt] = nil - } else if in.Status == "offline" { - data[c.OfflineAt] = now - } - if in.Id == 0 { - id, e := imtenant.NextSnowflakeID(txCtx) - if e != nil { - return e - } - in.Id = uint64(id) - data[c.Id] = id - data[c.TenantId] = tenantID - data[c.FlowerCode] = in.FlowerCode - data[c.CreatedAt] = now - if _, e = tx.Model(dao.FlowerCatalog.Table()).Data(data).Insert(); e != nil { - return gerror.Wrap(e, "新增花种失败") - } - } else { - if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).Data(data).Update(); e != nil { - return gerror.Wrap(e, "修改花种失败") - } - } - rc := dao.FlowerStageRule.Columns() - if _, e := tx.Model(dao.FlowerStageRule.Table()).Where(g.Map{rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id}).Delete(); e != nil { - return e - } - for _, rule := range in.StageRules { - id, e := imtenant.NextSnowflakeID(txCtx) - if e != nil { - return e - } - actions, _ := json.Marshal(rule.AllowedActions) - _, e = tx.Model(dao.FlowerStageRule.Table()).Data(g.Map{rc.Id: id, rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id, rc.Stage: rule.Stage, rc.NextStage: rule.NextStage, rc.RequiredPoints: rule.RequiredPoints, rc.WaterPoints: rule.WaterPoints, rc.FertilizePoints: rule.FertilizePoints, rc.AllowedActions: string(actions), rc.ImageFileId: rule.ImageFileId, rc.AnimationFileId: rule.AnimationFileId, rc.ProgressTextTemplate: rule.ProgressTextTemplate, rc.CompletionText: rule.CompletionText, rc.SortNo: rule.SortNo, rc.CreatedAt: now, rc.UpdatedAt: now}).Insert() - if e != nil { - return gerror.Wrap(e, "保存成长阶段失败") - } - } - return writeCultivateOperation(txCtx, tx, tenantID, operatorID, map[bool]string{true: "flower.edit", false: "flower.create"}[before.Id > 0], "flower", fmt.Sprint(in.Id), before, in, "", now) - }) + if operatorID <= 0 { + return cultivateadmin.Scope{}, gerror.New("当前管理员身份无效") + } + return cultivateadmin.Scope{ + TenantID: tenant.Id, TenantCode: tenant.Code, OperatorID: operatorID, + RoleKey: contexts.GetRoleKey(ctx), RequestID: gctx.CtxId(ctx), + }, nil } -func validateCultivateAssets(ctx context.Context, tenantID int64, in *sysin.CultivateFlowerEditInp) error { - seen := make(map[string]struct{}) - fileIDs := make([]string, 0, 1+len(in.StageRules)*2) - add := func(fileID string) { - fileID = strings.TrimSpace(fileID) - if fileID == "" { - return +func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) { + if err = in.Filter(ctx); err != nil { + return nil, 0, err + } + scope, err := cultivateAdminScope(ctx) + if err != nil { + return nil, 0, err + } + normalizeCultivatePage(&in.PageReq) + values := cultivatePageValues(in.Page, in.PerPage) + cultivateSetQuery(values, "flower_code", in.FlowerCode) + cultivateSetQuery(values, "flower_name", in.FlowerName) + cultivateSetQuery(values, "status", in.Status) + cultivateSetQuery(values, "config_version", in.ConfigVersion) + setTimeRange(values, in.CreatedAt) + var page cultivateadmin.Page[cultivateFlowerDTO] + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/flowers", values, &page); err != nil { + return nil, 0, err + } + list = make([]*sysin.CultivateFlowerListModel, 0, len(page.Items)) + for index := range page.Items { + item, convertErr := flowerListModel(&page.Items[index]) + if convertErr != nil { + return nil, 0, convertErr } - if _, ok := seen[fileID]; ok { - return - } - seen[fileID] = struct{}{} - fileIDs = append(fileIDs, fileID) + list = append(list, item) } - add(in.IconFileId) - for _, rule := range in.StageRules { - add(rule.ImageFileId) - add(rule.AnimationFileId) + return list, page.Total, nil +} + +func (s *sSysCultivateFlower) View(ctx context.Context, in *sysin.CultivateFlowerViewInp) (*sysin.CultivateFlowerViewModel, error) { + if err := in.Filter(ctx); err != nil { + return nil, err } - if len(fileIDs) == 0 { - return nil + scope, err := cultivateAdminScope(ctx) + if err != nil { + return nil, err } - tenant, err := resolveImAdminTenant(ctx, tenantID) + var item cultivateFlowerDTO + if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/flowers/"+strconv.FormatUint(in.Id, 10), nil, &item); err != nil { + return nil, err + } + return flowerViewModel(&item) +} + +func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowerEditInp) error { + if err := in.Filter(ctx); err != nil { + return err + } + scope, err := cultivateAdminScope(ctx) if err != nil { return err } - path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/files/validate" - for offset := 0; offset < len(fileIDs); offset += 20 { - end := offset + 20 - if end > len(fileIDs) { - end = len(fileIDs) - } - var out struct { - Items []struct { - FileId string `json:"file_id"` - ReviewStatus string `json:"review_status"` - } `json:"items"` - } - if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_id": 0, "file_ids": fileIDs[offset:end]}, &out); err != nil { - return gerror.Wrap(err, "校验花种素材失败") - } - for _, item := range out.Items { - if item.ReviewStatus == "risky" { - return gerror.Newf("素材 %s 审核未通过", item.FileId) - } - if item.ReviewStatus == "pending" { - return gerror.Newf("素材 %s 尚在审核中", item.FileId) - } + payload := flowerPayload(in) + var response struct { + ID string `json:"id"` + } + if in.Id == 0 { + err = cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/flowers", payload, &response) + } else { + err = cultivateadmin.DefaultClient(ctx).Put(ctx, scope, "/flowers/"+strconv.FormatUint(in.Id, 10), payload, &response) + } + if err != nil { + return err + } + if in.Id == 0 && response.ID != "" { + in.Id, err = strconv.ParseUint(response.ID, 10, 64) + if err != nil { + return gerror.Wrap(err, "解析养成服务花种ID失败") } } return nil } func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error { - tenantID := cultivateTenantID(ctx) - operatorID := contexts.GetUserId(ctx) - now := time.Now().UTC() - return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { - c := dao.FlowerCatalog.Columns() - var before entity.FlowerCatalog - if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil { - return e - } - if before.Id == 0 { - return gerror.New("花种不存在") - } - data := g.Map{c.Status: in.Status, c.ConfigVersion: "cat_" + now.Format("20060102_150405"), c.UpdatedAt: now} - if in.Status == "online" { - data[c.OnlineAt] = now - data[c.OfflineAt] = nil - } else if in.Status == "offline" { - data[c.OfflineAt] = now - } - if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(c.Id, in.Id).Data(data).Update(); e != nil { - return e - } - return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "flower.status", "flower", fmt.Sprint(in.Id), before, data, in.Reason, now) - }) + if err := in.Filter(ctx); err != nil { + return err + } + scope, err := cultivateAdminScope(ctx) + if err != nil { + return err + } + return cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/flowers/"+strconv.FormatUint(in.Id, 10)+"/status", map[string]any{ + "status": in.Status, "reason": strings.TrimSpace(in.Reason), + }, nil) } -func writeCultivateOperation(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, operation, targetType, targetID string, before, after any, reason string, now time.Time) error { - id, e := imtenant.NextSnowflakeID(ctx) - if e != nil { - return e +func flowerPayload(in *sysin.CultivateFlowerEditInp) map[string]any { + shareConfig := map[string]any{} + if in.ShareConfig != nil { + _ = json.Unmarshal([]byte(in.ShareConfig.MustToJsonString()), &shareConfig) } - beforeJSON, _ := json.Marshal(before) - afterJSON, _ := json.Marshal(after) - ipAddress, userAgent := "", "" - if request := ghttp.RequestFromCtx(ctx); request != nil { - ipAddress = location.GetClientIp(request) - userAgent = request.UserAgent() + rules := make([]map[string]any, 0, len(in.StageRules)) + for _, rule := range in.StageRules { + if rule == nil { + continue + } + rules = append(rules, map[string]any{ + "id": strconv.FormatUint(rule.Id, 10), "stage": rule.Stage, "next_stage": rule.NextStage, + "required_points": rule.RequiredPoints, "water_points": rule.WaterPoints, "fertilize_points": rule.FertilizePoints, + "allowed_actions": rule.AllowedActions, "image_file_id": rule.ImageFileId, "animation_file_id": rule.AnimationFileId, + "progress_text_template": rule.ProgressTextTemplate, "completion_text": rule.CompletionText, "sort_no": rule.SortNo, + }) + } + return map[string]any{ + "flower_code": in.FlowerCode, "flower_name": in.FlowerName, "flower_desc": in.FlowerDesc, + "icon_file_id": in.IconFileId, "status": in.Status, "daily_water_limit": in.DailyWaterLimit, + "daily_fertilize_limit": in.DailyFertilizeLimit, "share_config": shareConfig, "sort_no": in.SortNo, "stage_rules": rules, + } +} + +func flowerListModel(item *cultivateFlowerDTO) (*sysin.CultivateFlowerListModel, error) { + id, err := parseCultivateUint(item.ID, "花种ID") + if err != nil { + return nil, err + } + return &sysin.CultivateFlowerListModel{ + Id: id, FlowerCode: item.FlowerCode, FlowerName: item.FlowerName, FlowerDesc: item.FlowerDesc, + IconFileId: item.IconFileID, Status: item.Status, ConfigVersion: item.ConfigVersion, + DailyWaterLimit: item.DailyWaterLimit, DailyFertilizeLimit: item.DailyFertilizeLimit, + SortNo: item.SortNo, StageCount: item.StageCount, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + }, nil +} + +func flowerViewModel(item *cultivateFlowerDTO) (*sysin.CultivateFlowerViewModel, error) { + list, err := flowerListModel(item) + if err != nil { + return nil, err + } + shareConfig, _ := json.Marshal(item.ShareConfig) + result := &sysin.CultivateFlowerViewModel{ + CultivateFlowerEditInp: sysin.CultivateFlowerEditInp{ + Id: list.Id, FlowerCode: list.FlowerCode, FlowerName: list.FlowerName, FlowerDesc: list.FlowerDesc, + IconFileId: list.IconFileId, Status: list.Status, DailyWaterLimit: list.DailyWaterLimit, + DailyFertilizeLimit: list.DailyFertilizeLimit, ShareConfig: gjson.New(string(shareConfig)), SortNo: list.SortNo, + }, + ConfigVersion: list.ConfigVersion, CreatedAt: list.CreatedAt, UpdatedAt: list.UpdatedAt, + } + for _, rule := range item.StageRules { + id, parseErr := parseCultivateUint(rule.ID, "成长阶段ID") + if parseErr != nil { + return nil, parseErr + } + result.StageRules = append(result.StageRules, &sysin.CultivateStageRuleEditInp{ + Id: id, Stage: rule.Stage, NextStage: rule.NextStage, RequiredPoints: rule.RequiredPoints, + WaterPoints: rule.WaterPoints, FertilizePoints: rule.FertilizePoints, AllowedActions: rule.AllowedActions, + ImageFileId: rule.ImageFileID, AnimationFileId: rule.AnimationFileID, + ProgressTextTemplate: rule.ProgressTextTemplate, CompletionText: rule.CompletionText, SortNo: rule.SortNo, + }) + } + return result, nil +} + +func parseCultivateUint(value, field string) (uint64, error) { + id, err := strconv.ParseUint(strings.TrimSpace(value), 10, 64) + if err != nil || id == 0 { + return 0, gerror.Newf("养成服务返回的%s不正确", field) + } + return id, nil +} + +func normalizeCultivatePage(page *form.PageReq) { + if page.Page <= 0 { + page.Page = 1 + } + if page.PerPage <= 0 { + page.PerPage = 10 + } + if page.PerPage > 100 { + page.PerPage = 100 + } +} + +func cultivatePageValues(page, perPage int) url.Values { + return url.Values{"page": []string{strconv.Itoa(page)}, "per_page": []string{strconv.Itoa(perPage)}} +} + +func cultivateSetQuery(values url.Values, key, value string) { + if value = strings.TrimSpace(value); value != "" { + values.Set(key, value) + } +} + +func setTimeRange(values url.Values, value []*gtime.Time) { + if len(value) != 2 { + return + } + if value[0] != nil { + values.Set("created_from", value[0].Format(time.RFC3339)) + } + if value[1] != nil { + values.Set("created_to", value[1].Format(time.RFC3339)) } - c := dao.AdminOperationLog.Columns() - _, e = tx.Model(dao.AdminOperationLog.Table()).Data(g.Map{c.Id: id, c.TenantId: tenantID, c.OperatorId: operatorID, c.OperationType: operation, c.TargetType: targetType, c.TargetId: targetID, c.RequestId: gctx.CtxId(ctx), c.BeforeSnapshot: gjson.New(beforeJSON).MustToJsonString(), c.AfterSnapshot: gjson.New(afterJSON).MustToJsonString(), c.Reason: reason, c.IpAddress: ipAddress, c.UserAgent: userAgent, c.CreatedAt: now}).Insert() - return e } diff --git a/server/internal/model/do/admin_operation_log.go b/server/internal/model/do/admin_operation_log.go deleted file mode 100644 index c399a41..0000000 --- a/server/internal/model/do/admin_operation_log.go +++ /dev/null @@ -1,29 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// AdminOperationLog is the golang structure of table cv_admin_operation_log for DAO operations like Where/Data. -type AdminOperationLog struct { - g.Meta `orm:"table:cv_admin_operation_log, do:true"` - Id any // - TenantId any // - OperatorId any // - OperationType any // - TargetType any // - TargetId any // - RequestId any // - BeforeSnapshot *gjson.Json // - AfterSnapshot *gjson.Json // - Reason any // - IpAddress any // - UserAgent any // - CreatedAt *gtime.Time // -} diff --git a/server/internal/model/do/content_audit_log.go b/server/internal/model/do/content_audit_log.go deleted file mode 100644 index fefee99..0000000 --- a/server/internal/model/do/content_audit_log.go +++ /dev/null @@ -1,29 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// ContentAuditLog is the golang structure of table cv_content_audit_log for DAO operations like Where/Data. -type ContentAuditLog struct { - g.Meta `orm:"table:cv_content_audit_log, do:true"` - Id any // - TenantId any // - ObjectType any // - ObjectId any // - AuditRequestId any // - AuditSource any // - StatusBefore any // - StatusAfter any // - ResultCode any // - RiskLabels *gjson.Json // - OperatorId any // - OccurredAt *gtime.Time // - CreatedAt *gtime.Time // -} diff --git a/server/internal/model/do/feed_comment.go b/server/internal/model/do/feed_comment.go deleted file mode 100644 index d3813b0..0000000 --- a/server/internal/model/do/feed_comment.go +++ /dev/null @@ -1,29 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedComment is the golang structure of table cv_feed_comment for DAO operations like Where/Data. -type FeedComment struct { - g.Meta `orm:"table:cv_feed_comment, do:true"` - Id any // - TenantId any // - PostId any // - AuthorId any // - ContentText any // - Status any // - AuditStatus any // - AuditRequestId any // - DeletedBy any // - DeleteReason any // - PublishedAt *gtime.Time // - DeletedAt *gtime.Time // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/do/feed_media.go b/server/internal/model/do/feed_media.go deleted file mode 100644 index 89740e3..0000000 --- a/server/internal/model/do/feed_media.go +++ /dev/null @@ -1,29 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedMedia is the golang structure of table cv_feed_media for DAO operations like Where/Data. -type FeedMedia struct { - g.Meta `orm:"table:cv_feed_media, do:true"` - Id any // - TenantId any // - PostId any // - UserId any // - MediaType any // - FileId any // - ThumbnailFileId any // - Width any // - Height any // - SortNo any // - AuditStatus any // - AuditRequestId any // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/do/feed_post.go b/server/internal/model/do/feed_post.go deleted file mode 100644 index 2d29103..0000000 --- a/server/internal/model/do/feed_post.go +++ /dev/null @@ -1,35 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedPost is the golang structure of table cv_feed_post for DAO operations like Where/Data. -type FeedPost struct { - g.Meta `orm:"table:cv_feed_post, do:true"` - Id any // - TenantId any // - AuthorId any // - ContentText any // - FlowerCode any // - SourceType any // - SourceId any // - Visibility any // - Status any // - AuditStatus any // - AuditRequestId any // - RejectReasonCode any // - LikeCount any // - CommentCount any // - Version any // - PublishedAt *gtime.Time // - DeletedAt *gtime.Time // - TakenDownAt *gtime.Time // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/do/feed_report.go b/server/internal/model/do/feed_report.go deleted file mode 100644 index f728602..0000000 --- a/server/internal/model/do/feed_report.go +++ /dev/null @@ -1,32 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedReport is the golang structure of table cv_feed_report for DAO operations like Where/Data. -type FeedReport struct { - g.Meta `orm:"table:cv_feed_report, do:true"` - Id any // - TenantId any // - ReporterId any // - ReportedUserId any // - ObjectType any // - ObjectId any // - ReasonCode any // - Description any // - EvidenceFileIds *gjson.Json // - SourcePage any // - Status any // - HandlerId any // - HandleResult any // - HandledAt *gtime.Time // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/do/flower_catalog.go b/server/internal/model/do/flower_catalog.go deleted file mode 100644 index 09f6bf0..0000000 --- a/server/internal/model/do/flower_catalog.go +++ /dev/null @@ -1,32 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FlowerCatalog is the golang structure of table cv_flower_catalog for DAO operations like Where/Data. -type FlowerCatalog struct { - g.Meta `orm:"table:cv_flower_catalog, do:true"` - Id any // - TenantId any // - FlowerCode any // - FlowerName any // - FlowerDesc any // - IconFileId any // - Status any // - ConfigVersion any // - DailyWaterLimit any // - DailyFertilizeLimit any // - ShareConfig *gjson.Json // - SortNo any // - OnlineAt *gtime.Time // - OfflineAt *gtime.Time // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/do/flower_stage_rule.go b/server/internal/model/do/flower_stage_rule.go deleted file mode 100644 index 8c1bbd6..0000000 --- a/server/internal/model/do/flower_stage_rule.go +++ /dev/null @@ -1,32 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// FlowerStageRule is the golang structure of table cv_flower_stage_rule for DAO operations like Where/Data. -type FlowerStageRule struct { - g.Meta `orm:"table:cv_flower_stage_rule, do:true"` - Id any // - TenantId any // - FlowerCatalogId any // - Stage any // - NextStage any // - RequiredPoints any // - WaterPoints any // - FertilizePoints any // - AllowedActions *gjson.Json // - ImageFileId any // - AnimationFileId any // - ProgressTextTemplate any // - CompletionText any // - SortNo any // - CreatedAt *gtime.Time // - UpdatedAt *gtime.Time // -} diff --git a/server/internal/model/entity/admin_operation_log.go b/server/internal/model/entity/admin_operation_log.go deleted file mode 100644 index 8d00338..0000000 --- a/server/internal/model/entity/admin_operation_log.go +++ /dev/null @@ -1,27 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/os/gtime" -) - -// AdminOperationLog is the golang structure for table admin_operation_log. -type AdminOperationLog struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - OperatorId uint64 `json:"operatorId" orm:"operator_id" description:""` - OperationType string `json:"operationType" orm:"operation_type" description:""` - TargetType string `json:"targetType" orm:"target_type" description:""` - TargetId string `json:"targetId" orm:"target_id" description:""` - RequestId string `json:"requestId" orm:"request_id" description:""` - BeforeSnapshot *gjson.Json `json:"beforeSnapshot" orm:"before_snapshot" description:""` - AfterSnapshot *gjson.Json `json:"afterSnapshot" orm:"after_snapshot" description:""` - Reason string `json:"reason" orm:"reason" description:""` - IpAddress string `json:"ipAddress" orm:"ip_address" description:""` - UserAgent string `json:"userAgent" orm:"user_agent" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` -} diff --git a/server/internal/model/entity/content_audit_log.go b/server/internal/model/entity/content_audit_log.go deleted file mode 100644 index a2dacc5..0000000 --- a/server/internal/model/entity/content_audit_log.go +++ /dev/null @@ -1,27 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/os/gtime" -) - -// ContentAuditLog is the golang structure for table content_audit_log. -type ContentAuditLog struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - ObjectType string `json:"objectType" orm:"object_type" description:""` - ObjectId uint64 `json:"objectId" orm:"object_id" description:""` - AuditRequestId string `json:"auditRequestId" orm:"audit_request_id" description:""` - AuditSource string `json:"auditSource" orm:"audit_source" description:""` - StatusBefore string `json:"statusBefore" orm:"status_before" description:""` - StatusAfter string `json:"statusAfter" orm:"status_after" description:""` - ResultCode string `json:"resultCode" orm:"result_code" description:""` - RiskLabels *gjson.Json `json:"riskLabels" orm:"risk_labels" description:""` - OperatorId uint64 `json:"operatorId" orm:"operator_id" description:""` - OccurredAt *gtime.Time `json:"occurredAt" orm:"occurred_at" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` -} diff --git a/server/internal/model/entity/feed_comment.go b/server/internal/model/entity/feed_comment.go deleted file mode 100644 index 1d1b250..0000000 --- a/server/internal/model/entity/feed_comment.go +++ /dev/null @@ -1,27 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedComment is the golang structure for table feed_comment. -type FeedComment struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - PostId uint64 `json:"postId" orm:"post_id" description:""` - AuthorId uint64 `json:"authorId" orm:"author_id" description:""` - ContentText string `json:"contentText" orm:"content_text" description:""` - Status string `json:"status" orm:"status" description:""` - AuditStatus string `json:"auditStatus" orm:"audit_status" description:""` - AuditRequestId string `json:"auditRequestId" orm:"audit_request_id" description:""` - DeletedBy uint64 `json:"deletedBy" orm:"deleted_by" description:""` - DeleteReason string `json:"deleteReason" orm:"delete_reason" description:""` - PublishedAt *gtime.Time `json:"publishedAt" orm:"published_at" description:""` - DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/internal/model/entity/feed_media.go b/server/internal/model/entity/feed_media.go deleted file mode 100644 index 3fb3343..0000000 --- a/server/internal/model/entity/feed_media.go +++ /dev/null @@ -1,27 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedMedia is the golang structure for table feed_media. -type FeedMedia struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - PostId uint64 `json:"postId" orm:"post_id" description:""` - UserId uint64 `json:"userId" orm:"user_id" description:""` - MediaType string `json:"mediaType" orm:"media_type" description:""` - FileId string `json:"fileId" orm:"file_id" description:""` - ThumbnailFileId string `json:"thumbnailFileId" orm:"thumbnail_file_id" description:""` - Width uint `json:"width" orm:"width" description:""` - Height uint `json:"height" orm:"height" description:""` - SortNo uint `json:"sortNo" orm:"sort_no" description:""` - AuditStatus string `json:"auditStatus" orm:"audit_status" description:""` - AuditRequestId string `json:"auditRequestId" orm:"audit_request_id" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/internal/model/entity/feed_post.go b/server/internal/model/entity/feed_post.go deleted file mode 100644 index 87be5d2..0000000 --- a/server/internal/model/entity/feed_post.go +++ /dev/null @@ -1,33 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedPost is the golang structure for table feed_post. -type FeedPost struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - AuthorId uint64 `json:"authorId" orm:"author_id" description:""` - ContentText string `json:"contentText" orm:"content_text" description:""` - FlowerCode string `json:"flowerCode" orm:"flower_code" description:""` - SourceType string `json:"sourceType" orm:"source_type" description:""` - SourceId uint64 `json:"sourceId" orm:"source_id" description:""` - Visibility string `json:"visibility" orm:"visibility" description:""` - Status string `json:"status" orm:"status" description:""` - AuditStatus string `json:"auditStatus" orm:"audit_status" description:""` - AuditRequestId string `json:"auditRequestId" orm:"audit_request_id" description:""` - RejectReasonCode string `json:"rejectReasonCode" orm:"reject_reason_code" description:""` - LikeCount uint `json:"likeCount" orm:"like_count" description:""` - CommentCount uint `json:"commentCount" orm:"comment_count" description:""` - Version uint `json:"version" orm:"version" description:""` - PublishedAt *gtime.Time `json:"publishedAt" orm:"published_at" description:""` - DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` - TakenDownAt *gtime.Time `json:"takenDownAt" orm:"taken_down_at" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/internal/model/entity/feed_report.go b/server/internal/model/entity/feed_report.go deleted file mode 100644 index 68bd200..0000000 --- a/server/internal/model/entity/feed_report.go +++ /dev/null @@ -1,30 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/os/gtime" -) - -// FeedReport is the golang structure for table feed_report. -type FeedReport struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - ReporterId uint64 `json:"reporterId" orm:"reporter_id" description:""` - ReportedUserId uint64 `json:"reportedUserId" orm:"reported_user_id" description:""` - ObjectType string `json:"objectType" orm:"object_type" description:""` - ObjectId string `json:"objectId" orm:"object_id" description:""` - ReasonCode string `json:"reasonCode" orm:"reason_code" description:""` - Description string `json:"description" orm:"description" description:""` - EvidenceFileIds *gjson.Json `json:"evidenceFileIds" orm:"evidence_file_ids" description:""` - SourcePage string `json:"sourcePage" orm:"source_page" description:""` - Status string `json:"status" orm:"status" description:""` - HandlerId uint64 `json:"handlerId" orm:"handler_id" description:""` - HandleResult string `json:"handleResult" orm:"handle_result" description:""` - HandledAt *gtime.Time `json:"handledAt" orm:"handled_at" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/internal/model/entity/flower_catalog.go b/server/internal/model/entity/flower_catalog.go deleted file mode 100644 index 3703e7c..0000000 --- a/server/internal/model/entity/flower_catalog.go +++ /dev/null @@ -1,30 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/os/gtime" -) - -// FlowerCatalog is the golang structure for table flower_catalog. -type FlowerCatalog struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - FlowerCode string `json:"flowerCode" orm:"flower_code" description:""` - FlowerName string `json:"flowerName" orm:"flower_name" description:""` - FlowerDesc string `json:"flowerDesc" orm:"flower_desc" description:""` - IconFileId string `json:"iconFileId" orm:"icon_file_id" description:""` - Status string `json:"status" orm:"status" description:""` - ConfigVersion string `json:"configVersion" orm:"config_version" description:""` - DailyWaterLimit uint `json:"dailyWaterLimit" orm:"daily_water_limit" description:""` - DailyFertilizeLimit uint `json:"dailyFertilizeLimit" orm:"daily_fertilize_limit" description:""` - ShareConfig *gjson.Json `json:"shareConfig" orm:"share_config" description:""` - SortNo int `json:"sortNo" orm:"sort_no" description:""` - OnlineAt *gtime.Time `json:"onlineAt" orm:"online_at" description:""` - OfflineAt *gtime.Time `json:"offlineAt" orm:"offline_at" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/internal/model/entity/flower_stage_rule.go b/server/internal/model/entity/flower_stage_rule.go deleted file mode 100644 index bd9bfbd..0000000 --- a/server/internal/model/entity/flower_stage_rule.go +++ /dev/null @@ -1,30 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/os/gtime" -) - -// FlowerStageRule is the golang structure for table flower_stage_rule. -type FlowerStageRule struct { - Id uint64 `json:"id" orm:"id" description:""` - TenantId uint64 `json:"tenantId" orm:"tenant_id" description:""` - FlowerCatalogId uint64 `json:"flowerCatalogId" orm:"flower_catalog_id" description:""` - Stage string `json:"stage" orm:"stage" description:""` - NextStage string `json:"nextStage" orm:"next_stage" description:""` - RequiredPoints uint `json:"requiredPoints" orm:"required_points" description:""` - WaterPoints uint `json:"waterPoints" orm:"water_points" description:""` - FertilizePoints uint `json:"fertilizePoints" orm:"fertilize_points" description:""` - AllowedActions *gjson.Json `json:"allowedActions" orm:"allowed_actions" description:""` - ImageFileId string `json:"imageFileId" orm:"image_file_id" description:""` - AnimationFileId string `json:"animationFileId" orm:"animation_file_id" description:""` - ProgressTextTemplate string `json:"progressTextTemplate" orm:"progress_text_template" description:""` - CompletionText string `json:"completionText" orm:"completion_text" description:""` - SortNo int `json:"sortNo" orm:"sort_no" description:""` - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` -} diff --git a/server/manifest/config/config.template b/server/manifest/config/config.template index 18a38b5..b316873 100644 --- a/server/manifest/config/config.template +++ b/server/manifest/config/config.template @@ -240,9 +240,6 @@ database: link: "$MYSQL_DSN" debug: true Prefix: "hg_" - cultivate: - link: "$CULTIVATE_MYSQL_DSN" - debug: true cultivate: @@ -328,6 +325,11 @@ imWorkbenchApi: serviceSecret: "$IM_WORKBENCH_API_SECRET" timeout: "5s" +cultivateAdminApi: + baseUrl: "http://cultivate-server.cultivate-system.svc.cluster.local:8080" + serviceName: "im-manager" + timeout: "5s" + imConfigApi: serviceName: "$IM_CONFIG_API_SERVICE_NAME" serviceSecret: "$IM_CONFIG_API_SECRET"