feat: integrate cultivate admin internal APIs
This commit is contained in:
@@ -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 侧切换和删除直连代码。
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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:""`
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user