Compare commits
38
Commits
0596599db9
...
pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba837f4cae | ||
|
|
0553424ef9 | ||
|
|
64037863ef | ||
|
|
db2eb62277 | ||
|
|
a3527a6d22 | ||
|
|
8d6618dac1 | ||
|
|
2c5995073f | ||
|
|
315d97895b | ||
|
|
3834af70b5 | ||
|
|
667e62e97e | ||
|
|
55ae8b2acd | ||
|
|
5a7d8407bc | ||
|
|
837a03a87e | ||
|
|
0edbc63bc1 | ||
|
|
9e01f95a61 | ||
|
|
fce8dbbe32 | ||
|
|
43f3581b1c | ||
|
|
9924c1e522 | ||
|
|
e87ac92cd1 | ||
|
|
e24a04a920 | ||
|
|
32017a2fbf | ||
|
|
369898bd7c | ||
|
|
e002e45f82 | ||
|
|
abadcceb19 | ||
|
|
5a07f63716 | ||
|
|
6c338626d6 | ||
|
|
22adc13acf | ||
|
|
432e7ac51a | ||
|
|
dd12dbfe07 | ||
|
|
4fbc896d75 | ||
|
|
603e89e204 | ||
|
|
abd5eb162a | ||
|
|
935bfe17a0 | ||
|
|
671ac2e1f7 | ||
|
|
dc3c45cd53 | ||
|
|
9a9e30f94e | ||
|
|
85b564e2ac | ||
|
|
851eca97ed |
+53
-22
@@ -1,37 +1,68 @@
|
|||||||
name: Deploy im-manager (k3s test cluster)
|
name: Deploy im-manager production
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- pro
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
image_tag:
|
concurrency:
|
||||||
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)'
|
group: production-im-manager
|
||||||
required: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.olns.shop
|
||||||
|
IMAGE: git.olns.shop/pro/im_manager
|
||||||
|
SERVICE: im-manager
|
||||||
|
DEPLOY_HOST: 145.223.69.9
|
||||||
|
DEPLOY_USER: olns-deploy
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: deploy-host
|
runs-on: deploy-host
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 确定镜像tag
|
- name: Log in to Gitea Container Registry
|
||||||
id: vars
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ github.event.inputs.image_tag }}"
|
set -euo pipefail
|
||||||
[ -z "$TAG" ] && TAG=test
|
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin
|
||||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: 登录镜像仓库
|
- name: Build and push image
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin
|
env:
|
||||||
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
- name: 构建并推送镜像(前端+后端一体,自包含多阶段构建)
|
|
||||||
run: |
|
run: |
|
||||||
docker build -t git.118im.com/im_team/im-manager:${{ steps.vars.outputs.tag }} .
|
set -euo pipefail
|
||||||
docker push git.118im.com/im_team/im-manager:${{ steps.vars.outputs.tag }}
|
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
|
||||||
|
docker build -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
|
||||||
|
docker push "$IMAGE:pro"
|
||||||
|
docker push "$IMAGE:$SHORT_SHA"
|
||||||
|
|
||||||
- name: 部署到k3s测试集群
|
- name: Deploy Compose service
|
||||||
|
env:
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.kube
|
set -euo pipefail
|
||||||
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
|
install -d -m 700 "$HOME/.ssh"
|
||||||
chmod 600 ~/.kube/config
|
KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")"
|
||||||
kubectl rollout restart deployment/im-manager -n manager-system
|
cleanup() {
|
||||||
kubectl rollout status deployment/im-manager -n manager-system --timeout=120s
|
shred -u "$KEY_FILE" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_FILE"
|
||||||
|
chmod 600 "$KEY_FILE"
|
||||||
|
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
|
||||||
|
chmod 600 "$HOME/.ssh/known_hosts"
|
||||||
|
SSH_ARGS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=yes)
|
||||||
|
printf '%s' "$REGISTRY_TOKEN" | ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
|
"docker login git.olns.shop -u admin --password-stdin"
|
||||||
|
ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
|
"cd /opt/olns && docker compose pull im-manager && docker compose up -d --no-deps --wait im-manager"
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# IM Manager 发布冒烟测试记录
|
||||||
|
|
||||||
|
测试时间:2026-09-23 02:50 CST
|
||||||
|
|
||||||
|
测试环境:
|
||||||
|
|
||||||
|
- Manager:`https://admin.118im.com/admin`
|
||||||
|
- 登录账号:`potter`
|
||||||
|
- 角色:平台超管 `supreme_manager`
|
||||||
|
- 租户:`default`,`tenantId=1`
|
||||||
|
|
||||||
|
## 通过接口
|
||||||
|
|
||||||
|
以下接口返回 `code=0`:
|
||||||
|
|
||||||
|
| 模块 | 接口 | 数据量 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 租户列表 | `GET /imTenant/list?page=1&pageSize=10` | 8 |
|
||||||
|
| 租户详情 | `GET /imTenant/view?id=1` | 有数据 |
|
||||||
|
| 租户用量 | `GET /imTenant/usage?id=1` | 有数据 |
|
||||||
|
| 企业注册申请 | `GET /imTenantApplication/list?page=1&pageSize=10` | 10 |
|
||||||
|
| 企业注册详情 | `GET /imTenantApplication/view?id=4` | 有数据 |
|
||||||
|
| 企业反馈 | `GET /imTenantEntryFeedback/list?page=1&pageSize=10` | 3 |
|
||||||
|
| 用户列表 | `GET /imUser/list?tenantId=1&page=1&pageSize=10` | 10 |
|
||||||
|
| 群组列表 | `GET /imGroup/list?tenantId=1&page=1&pageSize=10` | 0 |
|
||||||
|
| 实名认证 | `GET /imRealname/list?tenantId=1&page=1&pageSize=10` | 1 |
|
||||||
|
| 群消息 | `GET /imGroupMessage/list?tenantId=1&page=1&pageSize=10` | 0 |
|
||||||
|
| 登录历史 | `GET /imUserLoginHistory/list?tenantId=1&page=1&pageSize=10` | 10 |
|
||||||
|
| 工作台概览 | `GET /imDashboard/overview?tenantId=1` | 有数据 |
|
||||||
|
| 趋势图 | `GET /imDashboard/trends?tenantId=1` | 7 |
|
||||||
|
| 通知模板 | `GET /imNotification/templateList?tenantId=1&page=1&pageSize=10` | 7 |
|
||||||
|
| 文件列表 | `GET /imFile/list?tenantId=1&page=1&pageSize=10` | 10 |
|
||||||
|
| 审计日志 | `GET /imAuditLog/list?tenantId=1&page=1&pageSize=10` | 0 |
|
||||||
|
| 外部配置 | `GET /imExternalConfig/list?tenantId=1&page=1&pageSize=20` | 5 |
|
||||||
|
| 文件策略 | `GET /imFilePolicy/list?tenantId=1&page=1&pageSize=10` | 6 |
|
||||||
|
| App 版本 | `GET /imAppRelease/list?page=1&pageSize=10` | 3 |
|
||||||
|
| 安全策略 | `GET /imSecurityPolicy/list?tenantId=1&page=1&pageSize=10` | 1 |
|
||||||
|
| 用户策略分组 | `GET /imUserPolicyGroup/list?tenantId=1&page=1&pageSize=10` | 3 |
|
||||||
|
| 敏感词分组 | `GET /imSensitiveWord/groupList?tenantId=1&page=1&pageSize=10` | 6 |
|
||||||
|
| 敏感词 | `GET /imSensitiveWord/list?tenantId=1&page=1&pageSize=10` | 10 |
|
||||||
|
| 举报原因 | `GET /imAbuseReason/list?tenantId=1&page=1&pageSize=10` | 10 |
|
||||||
|
| 注册邀请码 | `POST /imInvite/list` | 10 |
|
||||||
|
| Kafka 审计 | `GET /imKafkaAudit/list?page=1&pageSize=10` | 10 |
|
||||||
|
| Kafka 命令 | `GET /imKafkaCommand/list?page=1&pageSize=10&tenantId=1` | 10 |
|
||||||
|
| 死信列表 | `GET /imDeadLetter/list?page=1&pageSize=10&tenantId=1` | 0 |
|
||||||
|
|
||||||
|
## 发现问题
|
||||||
|
|
||||||
|
| 问题 | 现象 | 定位 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 注册配置平台角色被业务层拦截 | `GET /imRegistration/config/get?tenantId=1` 返回 `仅平台超级管理员可操作IM注册配置` | `admin` 账号可访问,`potter/supreme_manager` 不可访问;Manager 代码只认内置 super ID,未复用 IM 平台管理员判断 |
|
||||||
|
| 注册配置隐藏权限 SQL 不完整 | 测试库已补权限后可过菜单鉴权,但生成 SQL 缺少 `/imRegistration/config/get`、`/imRegistration/config/update` | 已补生成 SQL |
|
||||||
|
| 组织架构查询不可用 | `/imOrg/departmentList`、`/imOrg/memberList` 返回 `resource not found` | 直连 IM Server `/internal/admin-query/tenants/default/org/departments` 也是 404,线上 IM Server 未暴露该 internal query 资源 |
|
||||||
|
| 私聊消息查询不可用 | `/imPrivateMessage/list` 返回 `resource not found` | 直连 IM Server `/internal/admin-query/tenants/default/private-messages` 也是 404;同环境 `group-messages` 正常 |
|
||||||
|
| 登录失败日志不可用 | `/imLoginFailureLog/list` 返回 `resource not found` | 直连 IM Server `/internal/admin-query/tenants/default/login-failure-logs` 也是 404;IM Server 当前代码测试里也标注该资源已移除 |
|
||||||
|
| 工作台管理接口不可用 | `/imWorkbench/list`、`/imFeedbackConfig/get`、`/imMailboxConfig/get` 返回 `internal error` | `admin` 与 `potter` 均失败,基本排除菜单权限;需检查线上 Manager `imWorkbenchApi` 配置和 `im_workbench` 服务运行时配置 |
|
||||||
|
|
||||||
|
## 本次 Manager 修复
|
||||||
|
|
||||||
|
- 注册配置接口改为复用 `isImPlatformAdmin`,允许 `supreme_manager` 平台角色访问。
|
||||||
|
- 邀请码菜单生成 SQL 补充注册配置的隐藏权限。
|
||||||
|
- 本地发布配置已补 `imWorkbenchApi`,但 `server/manifest/config/config.yaml` 为 git 忽略文件,线上仍需确认实际 Secret/ConfigMap 是否包含工作台地址与密钥。
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/logic/sys ./internal/library/imworkbench
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:通过。
|
||||||
@@ -0,0 +1,697 @@
|
|||||||
|
# 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,
|
||||||
|
"wait_hours": 36,
|
||||||
|
"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` 与 `wait_hours` 不能同时为 0(2026-10-08 起:升级条件 = 积分就绪 AND 等待就绪,允许纯积分或纯等待阶段),且至少允许一个动作。
|
||||||
|
- 动作枚举:`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 侧切换和删除直连代码。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package cultivateapprelease
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListReq struct {
|
||||||
|
g.Meta `path:"/cultivate/app-release/list" method:"get" tags:"养成版本更新" summary:"获取养成客户端版本策略列表"`
|
||||||
|
sysin.ImAppReleaseListInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRes struct {
|
||||||
|
form.PageRes
|
||||||
|
List []*sysin.ImAppReleaseModel `json:"list" dc:"数据列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditReq struct {
|
||||||
|
g.Meta `path:"/cultivate/app-release/edit" method:"post" tags:"养成版本更新" summary:"新增/编辑养成客户端版本策略"`
|
||||||
|
sysin.ImAppReleaseEditInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditRes struct{}
|
||||||
@@ -35,8 +35,6 @@ type TenantModel struct {
|
|||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TenantDesc *string `json:"tenant_desc,omitempty"`
|
TenantDesc *string `json:"tenant_desc,omitempty"`
|
||||||
DBName string `json:"db_name"`
|
|
||||||
DBDSN string `json:"db_dsn"`
|
|
||||||
State int `json:"state"`
|
State int `json:"state"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
CreatedBy int64 `json:"created_by"`
|
CreatedBy int64 `json:"created_by"`
|
||||||
@@ -225,12 +223,14 @@ type TenantLoginIPWhitelistRes struct {
|
|||||||
|
|
||||||
type AppReleaseLatestReq struct {
|
type AppReleaseLatestReq struct {
|
||||||
g.Meta `path:"/internal/im-config/app/releases/latest" method:"get" tags:"IM内部配置" summary:"获取客户端最新版本策略"`
|
g.Meta `path:"/internal/im-config/app/releases/latest" method:"get" tags:"IM内部配置" summary:"获取客户端最新版本策略"`
|
||||||
|
AppCode string `json:"app_code" dc:"应用标识,缺省 im"`
|
||||||
Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"`
|
Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"`
|
||||||
Channel string `json:"channel" dc:"渠道"`
|
Channel string `json:"channel" dc:"渠道"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppReleaseLatestRes struct {
|
type AppReleaseLatestRes struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
AppCode string `json:"app_code"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
LatestVersion string `json:"latest_version"`
|
LatestVersion string `json:"latest_version"`
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ type SaveReq struct {
|
|||||||
type SaveRes struct {
|
type SaveRes struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
}
|
}
|
||||||
type PublishReq struct {
|
|
||||||
g.Meta `path:"/imSecurityPolicy/publish" method:"post" tags:"IM安全策略" summary:"发布安全策略"`
|
|
||||||
sysin.ImSecurityPolicyPublishInp
|
|
||||||
}
|
|
||||||
type PublishRes struct{}
|
|
||||||
type RestoreDefaultReq struct {
|
type RestoreDefaultReq struct {
|
||||||
g.Meta `path:"/imSecurityPolicy/restoreDefault" method:"post" tags:"IM安全策略" summary:"恢复默认安全策略"`
|
g.Meta `path:"/imSecurityPolicy/restoreDefault" method:"post" tags:"IM安全策略" summary:"恢复默认安全策略"`
|
||||||
sysin.ImSecurityPolicyRestoreInp
|
sysin.ImSecurityPolicyRestoreInp
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Package imtenantapplication
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package imtenantapplication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListReq 查询企业注册申请列表
|
||||||
|
type ListReq struct {
|
||||||
|
g.Meta `path:"/imTenantApplication/list" method:"get" tags:"企业注册申请" summary:"获取企业注册申请列表"`
|
||||||
|
sysin.ImTenantApplicationListInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRes struct {
|
||||||
|
form.PageRes
|
||||||
|
List []*sysin.ImTenantApplicationListModel `json:"list" dc:"数据列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewReq 获取企业注册申请指定信息
|
||||||
|
type ViewReq struct {
|
||||||
|
g.Meta `path:"/imTenantApplication/view" method:"get" tags:"企业注册申请" summary:"获取企业注册申请指定信息"`
|
||||||
|
sysin.ImTenantApplicationViewInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewRes struct {
|
||||||
|
*sysin.ImTenantApplicationViewModel
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Package imtenantentryfeedback
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package imtenantentryfeedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListReq 查询企业入口反馈列表
|
||||||
|
type ListReq struct {
|
||||||
|
g.Meta `path:"/imTenantEntryFeedback/list" method:"get" tags:"企业入口反馈" summary:"获取企业入口反馈列表"`
|
||||||
|
sysin.ImTenantEntryFeedbackListInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRes struct {
|
||||||
|
form.PageRes
|
||||||
|
List []*sysin.ImTenantEntryFeedbackListModel `json:"list" dc:"数据列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewReq 获取企业入口反馈指定信息
|
||||||
|
type ViewReq struct {
|
||||||
|
g.Meta `path:"/imTenantEntryFeedback/view" method:"get" tags:"企业入口反馈" summary:"获取企业入口反馈指定信息"`
|
||||||
|
sysin.ImTenantEntryFeedbackViewInp
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewRes struct {
|
||||||
|
*sysin.ImTenantEntryFeedbackViewModel
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package maoyucontent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 毛隅内容治理页面:概览、动态/评论审核、举报、操作日志。
|
||||||
|
// 页面协议与养成内容治理一致,通过 maoyu-server 内部接口处理毛隅业务。
|
||||||
|
|
||||||
|
type MaoyuContentDashboardReq struct {
|
||||||
|
g.Meta `path:"/maoyu/dashboard" method:"get" tags:"毛隅管理" summary:"获取毛隅管理概览"`
|
||||||
|
sysin.MaoyuDashboardInp
|
||||||
|
}
|
||||||
|
type MaoyuContentDashboardRes struct {
|
||||||
|
*sysin.MaoyuDashboardModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuContentListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/content/list" method:"get" tags:"毛隅管理" summary:"获取毛隅内容列表"`
|
||||||
|
*sysin.CultivateContentListInp
|
||||||
|
}
|
||||||
|
type MaoyuContentListRes struct {
|
||||||
|
List []*sysin.CultivateContentListModel `json:"list" dc:"内容列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuContentViewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/content/view" method:"get" tags:"毛隅管理" summary:"获取毛隅内容详情"`
|
||||||
|
*sysin.CultivateContentViewInp
|
||||||
|
}
|
||||||
|
type MaoyuContentViewRes struct {
|
||||||
|
*sysin.CultivateContentViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuContentReviewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/content/review" method:"post" tags:"毛隅管理" summary:"毛隅内容单条审核"`
|
||||||
|
*sysin.CultivateContentReviewInp
|
||||||
|
}
|
||||||
|
type MaoyuContentReviewRes struct{}
|
||||||
|
|
||||||
|
type MaoyuContentBatchReviewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/content/batch-review" method:"post" tags:"毛隅管理" summary:"毛隅内容批量审核"`
|
||||||
|
*sysin.CultivateContentBatchReviewInp
|
||||||
|
}
|
||||||
|
type MaoyuContentBatchReviewRes struct {
|
||||||
|
*sysin.CultivateContentBatchReviewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuReportListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/reports/list" method:"get" tags:"毛隅管理" summary:"获取毛隅举报列表"`
|
||||||
|
*sysin.CultivateReportListInp
|
||||||
|
}
|
||||||
|
type MaoyuReportListRes struct {
|
||||||
|
List []*sysin.CultivateReportListModel `json:"list" dc:"举报列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuReportViewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/reports/view" method:"get" tags:"毛隅管理" summary:"获取毛隅举报详情"`
|
||||||
|
*sysin.CultivateReportViewInp
|
||||||
|
}
|
||||||
|
type MaoyuReportViewRes struct {
|
||||||
|
*sysin.CultivateReportViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuReportHandleReq struct {
|
||||||
|
g.Meta `path:"/maoyu/reports/handle" method:"post" tags:"毛隅管理" summary:"处理毛隅举报"`
|
||||||
|
*sysin.CultivateReportHandleInp
|
||||||
|
}
|
||||||
|
type MaoyuReportHandleRes struct{}
|
||||||
|
|
||||||
|
type MaoyuLogListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/logs/list" method:"get" tags:"毛隅管理" summary:"获取毛隅操作日志列表"`
|
||||||
|
*sysin.CultivateOperationLogListInp
|
||||||
|
}
|
||||||
|
type MaoyuLogListRes struct {
|
||||||
|
List []*sysin.CultivateOperationLogListModel `json:"list" dc:"操作日志列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package maoyupet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 毛隅萌宠管理页面:互动动作配置、宠物档案、场景生成任务、互动通知。
|
||||||
|
|
||||||
|
type MaoyuActionConfigListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/action-configs/list" method:"get" tags:"毛隅管理" summary:"获取毛隅动作配置列表"`
|
||||||
|
*sysin.MaoyuActionConfigListInp
|
||||||
|
}
|
||||||
|
type MaoyuActionConfigListRes struct {
|
||||||
|
List []*sysin.MaoyuActionConfigListModel `json:"list" dc:"动作配置列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuActionConfigViewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/action-configs/view" method:"get" tags:"毛隅管理" summary:"获取毛隅动作配置详情"`
|
||||||
|
*sysin.MaoyuActionConfigViewInp
|
||||||
|
}
|
||||||
|
type MaoyuActionConfigViewRes struct {
|
||||||
|
*sysin.MaoyuActionConfigViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuActionConfigEditReq struct {
|
||||||
|
g.Meta `path:"/maoyu/action-configs/edit" method:"post" tags:"毛隅管理" summary:"新增或编辑毛隅动作配置"`
|
||||||
|
*sysin.MaoyuActionConfigEditInp
|
||||||
|
}
|
||||||
|
type MaoyuActionConfigEditRes struct{}
|
||||||
|
|
||||||
|
type MaoyuActionConfigStatusReq struct {
|
||||||
|
g.Meta `path:"/maoyu/action-configs/status" method:"post" tags:"毛隅管理" summary:"启用/停用毛隅动作配置"`
|
||||||
|
*sysin.MaoyuActionConfigStatusInp
|
||||||
|
}
|
||||||
|
type MaoyuActionConfigStatusRes struct{}
|
||||||
|
|
||||||
|
type MaoyuPetListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/pets/list" method:"get" tags:"毛隅管理" summary:"获取毛隅宠物档案列表"`
|
||||||
|
*sysin.MaoyuPetListInp
|
||||||
|
}
|
||||||
|
type MaoyuPetListRes struct {
|
||||||
|
List []*sysin.MaoyuPetListModel `json:"list" dc:"宠物档案列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuPetViewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/pets/view" method:"get" tags:"毛隅管理" summary:"获取毛隅宠物档案详情"`
|
||||||
|
*sysin.MaoyuPetViewInp
|
||||||
|
}
|
||||||
|
type MaoyuPetViewRes struct {
|
||||||
|
*sysin.MaoyuPetViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuJobListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/generation-jobs/list" method:"get" tags:"毛隅管理" summary:"获取毛隅生成任务列表"`
|
||||||
|
*sysin.MaoyuJobListInp
|
||||||
|
}
|
||||||
|
type MaoyuJobListRes struct {
|
||||||
|
List []*sysin.MaoyuJobListModel `json:"list" dc:"生成任务列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuJobViewReq struct {
|
||||||
|
g.Meta `path:"/maoyu/generation-jobs/view" method:"get" tags:"毛隅管理" summary:"获取毛隅生成任务详情"`
|
||||||
|
*sysin.MaoyuJobViewInp
|
||||||
|
}
|
||||||
|
type MaoyuJobViewRes struct {
|
||||||
|
*sysin.MaoyuJobViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaoyuNoticeListReq struct {
|
||||||
|
g.Meta `path:"/maoyu/notices/list" method:"get" tags:"毛隅管理" summary:"获取毛隅互动通知列表"`
|
||||||
|
*sysin.MaoyuNoticeListInp
|
||||||
|
}
|
||||||
|
type MaoyuNoticeListRes struct {
|
||||||
|
List []*sysin.MaoyuNoticeListModel `json:"list" dc:"互动通知列表"`
|
||||||
|
form.PageRes
|
||||||
|
}
|
||||||
@@ -34,17 +34,6 @@ gfcli:
|
|||||||
gJsonSupport: true
|
gJsonSupport: true
|
||||||
clear: false
|
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: # 生成业务配置
|
# service: # 生成业务配置
|
||||||
# srcFolder: "internal/logic"
|
# srcFolder: "internal/logic"
|
||||||
# dstFolder: "internal/service"
|
# dstFolder: "internal/service"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
dict.RegisterEnums("ImAppPlatformOptions", "IM客户端平台选项", ImAppPlatformOptions)
|
dict.RegisterEnums("ImAppPlatformOptions", "IM客户端平台选项", ImAppPlatformOptions)
|
||||||
dict.RegisterEnums("ImAppUpdateTypeOptions", "IM客户端更新类型选项", ImAppUpdateTypeOptions)
|
dict.RegisterEnums("ImAppUpdateTypeOptions", "IM客户端更新类型选项", ImAppUpdateTypeOptions)
|
||||||
|
dict.RegisterEnums("ImAppCodeOptions", "IM客户端应用标识选项", ImAppCodeOptions)
|
||||||
dict.RegisterEnums("ImAbuseTargetTypeOptions", "IM举报对象类型选项", ImAbuseTargetTypeOptions)
|
dict.RegisterEnums("ImAbuseTargetTypeOptions", "IM举报对象类型选项", ImAbuseTargetTypeOptions)
|
||||||
dict.RegisterEnums("ImAbuseReasonEnabledOptions", "IM举报原因启用状态", ImAbuseReasonEnabledOptions)
|
dict.RegisterEnums("ImAbuseReasonEnabledOptions", "IM举报原因启用状态", ImAbuseReasonEnabledOptions)
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,11 @@ const (
|
|||||||
ImAppPlatformAndroid = "android"
|
ImAppPlatformAndroid = "android"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ImAppCodeIm = "im"
|
||||||
|
ImAppCodeGarden = "garden"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ImAppUpdateTypeNone = "none"
|
ImAppUpdateTypeNone = "none"
|
||||||
ImAppUpdateTypeRecommended = "recommended"
|
ImAppUpdateTypeRecommended = "recommended"
|
||||||
@@ -34,6 +40,11 @@ var ImAppPlatformOptions = []*model.Option{
|
|||||||
dict.GenSuccessOption(ImAppPlatformAndroid, "Android"),
|
dict.GenSuccessOption(ImAppPlatformAndroid, "Android"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ImAppCodeOptions = []*model.Option{
|
||||||
|
dict.GenPrimaryOption(ImAppCodeIm, "讯工捷使"),
|
||||||
|
dict.GenSuccessOption(ImAppCodeGarden, "禾圃集"),
|
||||||
|
}
|
||||||
|
|
||||||
var ImAppUpdateTypeOptions = []*model.Option{
|
var ImAppUpdateTypeOptions = []*model.Option{
|
||||||
dict.GenDefaultOption(ImAppUpdateTypeNone, "无需更新"),
|
dict.GenDefaultOption(ImAppUpdateTypeNone, "无需更新"),
|
||||||
dict.GenWarningOption(ImAppUpdateTypeRecommended, "建议更新"),
|
dict.GenWarningOption(ImAppUpdateTypeRecommended, "建议更新"),
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/api/admin/cultivateapprelease"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CultivateAppRelease = cCultivateAppRelease{}
|
||||||
|
|
||||||
|
type cCultivateAppRelease struct{}
|
||||||
|
|
||||||
|
func (c *cCultivateAppRelease) List(ctx context.Context, req *cultivateapprelease.ListReq) (res *cultivateapprelease.ListRes, err error) {
|
||||||
|
list, totalCount, err := service.SysImAppRelease().List(ctx, &req.ImAppReleaseListInp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.ImAppReleaseModel{}
|
||||||
|
}
|
||||||
|
res = new(cultivateapprelease.ListRes)
|
||||||
|
res.List = list
|
||||||
|
res.PageRes.Pack(req, totalCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cCultivateAppRelease) Edit(ctx context.Context, req *cultivateapprelease.EditReq) (res *cultivateapprelease.EditRes, err error) {
|
||||||
|
err = service.SysImAppRelease().Edit(ctx, &req.ImAppReleaseEditInp)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -38,10 +38,6 @@ func (c *cImSecurityPolicy) Save(ctx context.Context, req *imsecuritypolicy.Save
|
|||||||
}
|
}
|
||||||
return &imsecuritypolicy.SaveRes{Id: id}, nil
|
return &imsecuritypolicy.SaveRes{Id: id}, nil
|
||||||
}
|
}
|
||||||
func (c *cImSecurityPolicy) Publish(ctx context.Context, req *imsecuritypolicy.PublishReq) (res *imsecuritypolicy.PublishRes, err error) {
|
|
||||||
err = service.SysImSecurityPolicy().Publish(ctx, &req.ImSecurityPolicyPublishInp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
func (c *cImSecurityPolicy) RestoreDefault(ctx context.Context, req *imsecuritypolicy.RestoreDefaultReq) (res *imsecuritypolicy.RestoreDefaultRes, err error) {
|
func (c *cImSecurityPolicy) RestoreDefault(ctx context.Context, req *imsecuritypolicy.RestoreDefaultReq) (res *imsecuritypolicy.RestoreDefaultRes, err error) {
|
||||||
id, err := service.SysImSecurityPolicy().RestoreDefault(ctx, &req.ImSecurityPolicyRestoreInp)
|
id, err := service.SysImSecurityPolicy().RestoreDefault(ctx, &req.ImSecurityPolicyRestoreInp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Package sys
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/api/admin/imtenantapplication"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ImTenantApplication = cImTenantApplication{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type cImTenantApplication struct{}
|
||||||
|
|
||||||
|
// List 查看企业注册申请列表
|
||||||
|
func (c *cImTenantApplication) List(ctx context.Context, req *imtenantapplication.ListReq) (res *imtenantapplication.ListRes, err error) {
|
||||||
|
list, totalCount, err := service.SysImTenantApplication().List(ctx, &req.ImTenantApplicationListInp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.ImTenantApplicationListModel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = new(imtenantapplication.ListRes)
|
||||||
|
res.List = list
|
||||||
|
res.PageRes.Pack(req, totalCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// View 获取指定企业注册申请信息
|
||||||
|
func (c *cImTenantApplication) View(ctx context.Context, req *imtenantapplication.ViewReq) (res *imtenantapplication.ViewRes, err error) {
|
||||||
|
data, err := service.SysImTenantApplication().View(ctx, &req.ImTenantApplicationViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res = new(imtenantapplication.ViewRes)
|
||||||
|
res.ImTenantApplicationViewModel = data
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Package sys
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/api/admin/imtenantentryfeedback"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ImTenantEntryFeedback = cImTenantEntryFeedback{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type cImTenantEntryFeedback struct{}
|
||||||
|
|
||||||
|
// List 查看企业入口反馈列表
|
||||||
|
func (c *cImTenantEntryFeedback) List(ctx context.Context, req *imtenantentryfeedback.ListReq) (res *imtenantentryfeedback.ListRes, err error) {
|
||||||
|
list, totalCount, err := service.SysImTenantEntryFeedback().List(ctx, &req.ImTenantEntryFeedbackListInp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.ImTenantEntryFeedbackListModel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = new(imtenantentryfeedback.ListRes)
|
||||||
|
res.List = list
|
||||||
|
res.PageRes.Pack(req, totalCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// View 获取指定企业入口反馈信息
|
||||||
|
func (c *cImTenantEntryFeedback) View(ctx context.Context, req *imtenantentryfeedback.ViewReq) (res *imtenantentryfeedback.ViewRes, err error) {
|
||||||
|
data, err := service.SysImTenantEntryFeedback().View(ctx, &req.ImTenantEntryFeedbackViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res = new(imtenantentryfeedback.ViewRes)
|
||||||
|
res.ImTenantEntryFeedbackViewModel = data
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"hotgo/api/admin/maoyucontent"
|
||||||
|
"hotgo/api/admin/maoyupet"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 毛隅管理页面控制器:内容治理 + 萌宠管理,业务经 maoyu-server 内部接口处理。
|
||||||
|
|
||||||
|
var (
|
||||||
|
MaoyuContent = cMaoyuContent{}
|
||||||
|
MaoyuPet = cMaoyuPet{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
cMaoyuContent struct{}
|
||||||
|
cMaoyuPet struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- 内容治理 ----
|
||||||
|
|
||||||
|
func (*cMaoyuContent) Dashboard(ctx context.Context, req *maoyucontent.MaoyuContentDashboardReq) (res *maoyucontent.MaoyuContentDashboardRes, err error) {
|
||||||
|
data, err := service.SysMaoyuContent().Dashboard(ctx, &req.MaoyuDashboardInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyucontent.MaoyuContentDashboardRes{MaoyuDashboardModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) List(ctx context.Context, req *maoyucontent.MaoyuContentListReq) (res *maoyucontent.MaoyuContentListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuContent().List(ctx, req.CultivateContentListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.CultivateContentListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyucontent.MaoyuContentListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) View(ctx context.Context, req *maoyucontent.MaoyuContentViewReq) (res *maoyucontent.MaoyuContentViewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuContent().View(ctx, req.CultivateContentViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyucontent.MaoyuContentViewRes{CultivateContentViewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) Review(ctx context.Context, req *maoyucontent.MaoyuContentReviewReq) (res *maoyucontent.MaoyuContentReviewRes, err error) {
|
||||||
|
err = service.SysMaoyuContent().Review(ctx, req.CultivateContentReviewInp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) BatchReview(ctx context.Context, req *maoyucontent.MaoyuContentBatchReviewReq) (res *maoyucontent.MaoyuContentBatchReviewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuContent().BatchReview(ctx, req.CultivateContentBatchReviewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyucontent.MaoyuContentBatchReviewRes{CultivateContentBatchReviewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) ReportList(ctx context.Context, req *maoyucontent.MaoyuReportListReq) (res *maoyucontent.MaoyuReportListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuContent().ReportList(ctx, req.CultivateReportListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.CultivateReportListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyucontent.MaoyuReportListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) ReportView(ctx context.Context, req *maoyucontent.MaoyuReportViewReq) (res *maoyucontent.MaoyuReportViewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuContent().ReportView(ctx, req.CultivateReportViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyucontent.MaoyuReportViewRes{CultivateReportViewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) ReportHandle(ctx context.Context, req *maoyucontent.MaoyuReportHandleReq) (res *maoyucontent.MaoyuReportHandleRes, err error) {
|
||||||
|
err = service.SysMaoyuContent().ReportHandle(ctx, req.CultivateReportHandleInp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuContent) LogList(ctx context.Context, req *maoyucontent.MaoyuLogListReq) (res *maoyucontent.MaoyuLogListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuContent().LogList(ctx, req.CultivateOperationLogListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.CultivateOperationLogListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyucontent.MaoyuLogListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 萌宠管理 ----
|
||||||
|
|
||||||
|
func (*cMaoyuPet) ActionConfigList(ctx context.Context, req *maoyupet.MaoyuActionConfigListReq) (res *maoyupet.MaoyuActionConfigListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuPet().ActionConfigList(ctx, req.MaoyuActionConfigListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.MaoyuActionConfigListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyupet.MaoyuActionConfigListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) ActionConfigView(ctx context.Context, req *maoyupet.MaoyuActionConfigViewReq) (res *maoyupet.MaoyuActionConfigViewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuPet().ActionConfigView(ctx, req.MaoyuActionConfigViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyupet.MaoyuActionConfigViewRes{MaoyuActionConfigViewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) ActionConfigEdit(ctx context.Context, req *maoyupet.MaoyuActionConfigEditReq) (res *maoyupet.MaoyuActionConfigEditRes, err error) {
|
||||||
|
err = service.SysMaoyuPet().ActionConfigEdit(ctx, req.MaoyuActionConfigEditInp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) ActionConfigStatus(ctx context.Context, req *maoyupet.MaoyuActionConfigStatusReq) (res *maoyupet.MaoyuActionConfigStatusRes, err error) {
|
||||||
|
err = service.SysMaoyuPet().ActionConfigStatus(ctx, req.MaoyuActionConfigStatusInp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) PetList(ctx context.Context, req *maoyupet.MaoyuPetListReq) (res *maoyupet.MaoyuPetListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuPet().PetList(ctx, req.MaoyuPetListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.MaoyuPetListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyupet.MaoyuPetListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) PetView(ctx context.Context, req *maoyupet.MaoyuPetViewReq) (res *maoyupet.MaoyuPetViewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuPet().PetView(ctx, req.MaoyuPetViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyupet.MaoyuPetViewRes{MaoyuPetViewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) JobList(ctx context.Context, req *maoyupet.MaoyuJobListReq) (res *maoyupet.MaoyuJobListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuPet().JobList(ctx, req.MaoyuJobListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.MaoyuJobListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyupet.MaoyuJobListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) JobView(ctx context.Context, req *maoyupet.MaoyuJobViewReq) (res *maoyupet.MaoyuJobViewRes, err error) {
|
||||||
|
data, err := service.SysMaoyuPet().JobView(ctx, req.MaoyuJobViewInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &maoyupet.MaoyuJobViewRes{MaoyuJobViewModel: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*cMaoyuPet) NoticeList(ctx context.Context, req *maoyupet.MaoyuNoticeListReq) (res *maoyupet.MaoyuNoticeListRes, err error) {
|
||||||
|
list, total, err := service.SysMaoyuPet().NoticeList(ctx, req.MaoyuNoticeListInp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []*sysin.MaoyuNoticeListModel{}
|
||||||
|
}
|
||||||
|
res = &maoyupet.MaoyuNoticeListRes{List: list}
|
||||||
|
res.PageRes.Pack(req, total)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -178,17 +178,8 @@ func (c *cConfig) ModerationPolicy(ctx context.Context, req *api.ModerationPolic
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res = defaultModerationPolicy(tenant)
|
res = defaultModerationPolicy(tenant)
|
||||||
var tenantConfig struct {
|
res.AliyunEnabled = tenant.AliyunModerationEnabled == 1
|
||||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
|
res.LocalWordsEnabled = tenant.LocalModerationEnabled == 1
|
||||||
LocalModerationEnabled int `json:"local_moderation_enabled"`
|
|
||||||
}
|
|
||||||
if err = imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
|
||||||
Fields("aliyun_moderation_enabled", "local_moderation_enabled").
|
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Scan(&tenantConfig); err != nil {
|
|
||||||
return nil, gerror.Wrap(err, "获取商户内容审核开关失败")
|
|
||||||
}
|
|
||||||
res.AliyunEnabled = tenantConfig.AliyunModerationEnabled == 1
|
|
||||||
res.LocalWordsEnabled = tenantConfig.LocalModerationEnabled == 1
|
|
||||||
res.Enabled = res.AliyunEnabled || res.LocalWordsEnabled
|
res.Enabled = res.AliyunEnabled || res.LocalWordsEnabled
|
||||||
columns := dao.ImSecurityPolicy.Columns()
|
columns := dao.ImSecurityPolicy.Columns()
|
||||||
var row struct {
|
var row struct {
|
||||||
@@ -333,18 +324,7 @@ func (c *cConfig) RealnamePolicy(ctx context.Context, req *api.RealnamePolicyReq
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res = defaultRealnamePolicy(tenant.Id)
|
res = defaultRealnamePolicy(tenant.Id)
|
||||||
var row *struct {
|
if tenant.RealnameReviewPolicy == 1 {
|
||||||
RealnameReviewPolicy int `json:"realname_review_policy"`
|
|
||||||
}
|
|
||||||
if err = imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
|
||||||
Fields("realname_review_policy").
|
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Scan(&row); err != nil {
|
|
||||||
return nil, gerror.Wrap(err, "获取租户实名认证策略失败")
|
|
||||||
}
|
|
||||||
if row == nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
if row.RealnameReviewPolicy == 1 {
|
|
||||||
res.RealnameReviewPolicy = 1
|
res.RealnameReviewPolicy = 1
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -488,8 +468,15 @@ func (c *cConfig) TenantLoginIPWhitelist(ctx context.Context, req *api.TenantLog
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *cConfig) AppReleaseLatest(ctx context.Context, req *api.AppReleaseLatestReq) (res *api.AppReleaseLatestRes, err error) {
|
func (c *cConfig) AppReleaseLatest(ctx context.Context, req *api.AppReleaseLatestReq) (res *api.AppReleaseLatestRes, err error) {
|
||||||
|
appCode := strings.ToLower(strings.TrimSpace(req.AppCode))
|
||||||
platform := strings.ToLower(strings.TrimSpace(req.Platform))
|
platform := strings.ToLower(strings.TrimSpace(req.Platform))
|
||||||
channel := strings.ToLower(strings.TrimSpace(req.Channel))
|
channel := strings.ToLower(strings.TrimSpace(req.Channel))
|
||||||
|
if appCode == "" {
|
||||||
|
appCode = "im"
|
||||||
|
}
|
||||||
|
if appCode != "im" && appCode != "garden" {
|
||||||
|
return nil, gerror.New("应用标识不合法")
|
||||||
|
}
|
||||||
if platform != "ios" && platform != "android" {
|
if platform != "ios" && platform != "android" {
|
||||||
return nil, gerror.New("客户端平台不合法")
|
return nil, gerror.New("客户端平台不合法")
|
||||||
}
|
}
|
||||||
@@ -501,6 +488,7 @@ func (c *cConfig) AppReleaseLatest(ctx context.Context, req *api.AppReleaseLates
|
|||||||
}
|
}
|
||||||
var row struct {
|
var row struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
AppCode string `json:"app_code"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
LatestVersion string `json:"latest_version"`
|
LatestVersion string `json:"latest_version"`
|
||||||
@@ -516,6 +504,7 @@ func (c *cConfig) AppReleaseLatest(ctx context.Context, req *api.AppReleaseLates
|
|||||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
err = imtenant.RegistryDB().Model("im_app_releases").Safe().Ctx(ctx).
|
err = imtenant.RegistryDB().Model("im_app_releases").Safe().Ctx(ctx).
|
||||||
|
Where("app_code", appCode).
|
||||||
Where("platform", platform).Where("channel", channel).Limit(1).Scan(&row)
|
Where("platform", platform).Where("channel", channel).Limit(1).Scan(&row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "获取IM客户端版本策略失败")
|
return nil, gerror.Wrap(err, "获取IM客户端版本策略失败")
|
||||||
@@ -524,7 +513,7 @@ func (c *cConfig) AppReleaseLatest(ctx context.Context, req *api.AppReleaseLates
|
|||||||
return nil, gerror.New("IM客户端版本策略不存在")
|
return nil, gerror.New("IM客户端版本策略不存在")
|
||||||
}
|
}
|
||||||
return &api.AppReleaseLatestRes{
|
return &api.AppReleaseLatestRes{
|
||||||
ID: row.ID, Platform: row.Platform, Channel: row.Channel,
|
ID: row.ID, AppCode: row.AppCode, Platform: row.Platform, Channel: row.Channel,
|
||||||
LatestVersion: row.LatestVersion, MinSupportedVersion: row.MinSupportedVersion,
|
LatestVersion: row.LatestVersion, MinSupportedVersion: row.MinSupportedVersion,
|
||||||
UpdateType: row.UpdateType, ReleaseNotes: row.ReleaseNotes, StoreURL: row.StoreURL,
|
UpdateType: row.UpdateType, ReleaseNotes: row.ReleaseNotes, StoreURL: row.StoreURL,
|
||||||
PackageURL: row.PackageURL, Checksum: row.Checksum, RemindAfterSeconds: row.RemindAfterSeconds,
|
PackageURL: row.PackageURL, Checksum: row.Checksum, RemindAfterSeconds: row.RemindAfterSeconds,
|
||||||
@@ -553,8 +542,6 @@ func tenantToModel(tenant *entity.ImTenant) *api.TenantModel {
|
|||||||
Code: tenant.Code,
|
Code: tenant.Code,
|
||||||
Name: tenant.Name,
|
Name: tenant.Name,
|
||||||
TenantDesc: &desc,
|
TenantDesc: &desc,
|
||||||
DBName: tenant.DbName,
|
|
||||||
DBDSN: tenant.DbDsn,
|
|
||||||
State: tenant.State,
|
State: tenant.State,
|
||||||
CreatedAt: gtimeToRFC3339(tenant.CreatedAt),
|
CreatedAt: gtimeToRFC3339(tenant.CreatedAt),
|
||||||
CreatedBy: tenant.CreatedBy,
|
CreatedBy: tenant.CreatedBy,
|
||||||
|
|||||||
@@ -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,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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// imTenantConfigDao is the data access object for the table im_tenant_config.
|
|
||||||
// You can define custom methods on it to extend its functionality as needed.
|
|
||||||
type imTenantConfigDao struct {
|
|
||||||
*internal.ImTenantConfigDao
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ImTenantConfig is a globally accessible object for table im_tenant_config operations.
|
|
||||||
ImTenantConfig = imTenantConfigDao{internal.NewImTenantConfigDao()}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -25,8 +25,17 @@ type ImTenantColumns struct {
|
|||||||
Code string //
|
Code string //
|
||||||
Name string //
|
Name string //
|
||||||
TenantDesc string //
|
TenantDesc string //
|
||||||
DbName string //
|
MaxUsers string // 0表示ä¸é™åˆ¶
|
||||||
DbDsn string //
|
MaxGroups string // 0表示ä¸é™åˆ¶
|
||||||
|
MaxGroupMembers string // 0表示ä¸é™åˆ¶
|
||||||
|
RealnameReviewPolicy string // 实åå®¡æ ¸ç–略:0自动通过,1è½¬äººå·¥å®¡æ ¸
|
||||||
|
AliyunModerationEnabled string // 是å¦å¯ç”¨é˜¿é‡Œäº‘å†…å®¹å®¡æ ¸
|
||||||
|
LocalModerationEnabled string // 是å¦å¯ç”¨æœ¬åœ°å†…å®¹å®¡æ ¸
|
||||||
|
MessageEncryptionEnabled string // 是å¦å¯ç”¨æ¶ˆæ¯åР坆
|
||||||
|
DefaultPolicyGroupId string // 默认用户ç–略组
|
||||||
|
SecurityPolicyVersion string // å·²å‘布安全ç–略版本
|
||||||
|
GlobalFriendEnabled string // å…¨å±€åŠ å¥½å‹å¼€å…³
|
||||||
|
MessageCurfewEnabled string // 消æ¯å®µç¦å¼€å…³
|
||||||
State string //
|
State string //
|
||||||
CreatedAt string //
|
CreatedAt string //
|
||||||
CreatedBy string //
|
CreatedBy string //
|
||||||
@@ -40,8 +49,17 @@ var imTenantColumns = ImTenantColumns{
|
|||||||
Code: "code",
|
Code: "code",
|
||||||
Name: "name",
|
Name: "name",
|
||||||
TenantDesc: "tenant_desc",
|
TenantDesc: "tenant_desc",
|
||||||
DbName: "db_name",
|
MaxUsers: "max_users",
|
||||||
DbDsn: "db_dsn",
|
MaxGroups: "max_groups",
|
||||||
|
MaxGroupMembers: "max_group_members",
|
||||||
|
RealnameReviewPolicy: "realname_review_policy",
|
||||||
|
AliyunModerationEnabled: "aliyun_moderation_enabled",
|
||||||
|
LocalModerationEnabled: "local_moderation_enabled",
|
||||||
|
MessageEncryptionEnabled: "message_encryption_enabled",
|
||||||
|
DefaultPolicyGroupId: "default_policy_group_id",
|
||||||
|
SecurityPolicyVersion: "security_policy_version",
|
||||||
|
GlobalFriendEnabled: "global_friend_enabled",
|
||||||
|
MessageCurfewEnabled: "message_curfew_enabled",
|
||||||
State: "state",
|
State: "state",
|
||||||
CreatedAt: "created_at",
|
CreatedAt: "created_at",
|
||||||
CreatedBy: "created_by",
|
CreatedBy: "created_by",
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImTenantConfigDao is the data access object for the table im_tenant_config.
|
|
||||||
type ImTenantConfigDao 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 ImTenantConfigColumns // columns contains all the column names of Table for convenient usage.
|
|
||||||
handlers []gdb.ModelHandler // handlers for customized model modification.
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImTenantConfigColumns defines and stores column names for the table im_tenant_config.
|
|
||||||
type ImTenantConfigColumns struct {
|
|
||||||
Id string //
|
|
||||||
TenantId string //
|
|
||||||
MaxUsers string // 0 means unlimited
|
|
||||||
MaxGroups string // 0 means unlimited
|
|
||||||
MaxGroupMembers string // 0 means unlimited
|
|
||||||
DefaultPolicyGroupId string // Default user policy group
|
|
||||||
SecurityPolicyVersion string // Published security policy version
|
|
||||||
GlobalFriendEnabled string // Published global friend switch
|
|
||||||
MessageCurfewEnabled string // Published message curfew switch
|
|
||||||
CreatedAt string //
|
|
||||||
CreatedBy string //
|
|
||||||
UpdatedAt string //
|
|
||||||
UpdatedBy string //
|
|
||||||
}
|
|
||||||
|
|
||||||
// imTenantConfigColumns holds the columns for the table im_tenant_config.
|
|
||||||
var imTenantConfigColumns = ImTenantConfigColumns{
|
|
||||||
Id: "id",
|
|
||||||
TenantId: "tenant_id",
|
|
||||||
MaxUsers: "max_users",
|
|
||||||
MaxGroups: "max_groups",
|
|
||||||
MaxGroupMembers: "max_group_members",
|
|
||||||
DefaultPolicyGroupId: "default_policy_group_id",
|
|
||||||
SecurityPolicyVersion: "security_policy_version",
|
|
||||||
GlobalFriendEnabled: "global_friend_enabled",
|
|
||||||
MessageCurfewEnabled: "message_curfew_enabled",
|
|
||||||
CreatedAt: "created_at",
|
|
||||||
CreatedBy: "created_by",
|
|
||||||
UpdatedAt: "updated_at",
|
|
||||||
UpdatedBy: "updated_by",
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewImTenantConfigDao creates and returns a new DAO object for table data access.
|
|
||||||
func NewImTenantConfigDao(handlers ...gdb.ModelHandler) *ImTenantConfigDao {
|
|
||||||
return &ImTenantConfigDao{
|
|
||||||
group: "default",
|
|
||||||
table: "im_tenant_config",
|
|
||||||
columns: imTenantConfigColumns,
|
|
||||||
handlers: handlers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB retrieves and returns the underlying raw database management object of the current DAO.
|
|
||||||
func (dao *ImTenantConfigDao) DB() gdb.DB {
|
|
||||||
return g.DB(dao.group)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table returns the table name of the current DAO.
|
|
||||||
func (dao *ImTenantConfigDao) Table() string {
|
|
||||||
return dao.table
|
|
||||||
}
|
|
||||||
|
|
||||||
// Columns returns all column names of the current DAO.
|
|
||||||
func (dao *ImTenantConfigDao) Columns() ImTenantConfigColumns {
|
|
||||||
return dao.columns
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group returns the database configuration group name of the current DAO.
|
|
||||||
func (dao *ImTenantConfigDao) 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 *ImTenantConfigDao) 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 *ImTenantConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
|
|
||||||
return dao.Ctx(ctx).Transaction(ctx, f)
|
|
||||||
}
|
|
||||||
@@ -10,13 +10,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
|
"hotgo/internal/library/cache"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/casbin/casbin/v2"
|
"github.com/casbin/casbin/v2"
|
||||||
"github.com/casbin/casbin/v2/model"
|
"github.com/casbin/casbin/v2/model"
|
||||||
|
casbinutil "github.com/casbin/casbin/v2/util"
|
||||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||||
"github.com/gogf/gf/v2/container/gvar"
|
"github.com/gogf/gf/v2/container/gvar"
|
||||||
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gfile"
|
"github.com/gogf/gf/v2/os/gfile"
|
||||||
"github.com/gogf/gf/v2/os/gres"
|
"github.com/gogf/gf/v2/os/gres"
|
||||||
@@ -33,6 +38,14 @@ const (
|
|||||||
|
|
||||||
var Enforcer *casbin.Enforcer
|
var Enforcer *casbin.Enforcer
|
||||||
|
|
||||||
|
const permissionCacheKey = "admin:role:permissions"
|
||||||
|
|
||||||
|
type PermissionPolicy struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Perm string `json:"perm"`
|
||||||
|
ActRegexp string `json:"actRegexp"`
|
||||||
|
}
|
||||||
|
|
||||||
// InitEnforcer 初始化
|
// InitEnforcer 初始化
|
||||||
func InitEnforcer(ctx context.Context) {
|
func InitEnforcer(ctx context.Context) {
|
||||||
var (
|
var (
|
||||||
@@ -69,6 +82,9 @@ func InitEnforcer(ctx context.Context) {
|
|||||||
g.Log().Panicf(ctx, "casbin NewEnforcer err:%v", err)
|
g.Log().Panicf(ctx, "casbin NewEnforcer err:%v", err)
|
||||||
}
|
}
|
||||||
loadPermissions(ctx)
|
loadPermissions(ctx)
|
||||||
|
if err = RefreshPermissionCache(ctx); err != nil {
|
||||||
|
g.Log().Panicf(ctx, "casbin RefreshPermissionCache err:%v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDbLink 获取数据库链接
|
// GetDbLink 获取数据库链接
|
||||||
@@ -91,15 +107,13 @@ func getDbLink(ctx context.Context) *gvar.Var {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPermissions(ctx context.Context) {
|
func queryPermissionPolicies(ctx context.Context) (policies []*PermissionPolicy, err error) {
|
||||||
type Policy struct {
|
type RolePermission struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Permissions string `json:"permissions"`
|
Permissions string `json:"permissions"`
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
rules [][]string
|
records []*RolePermission
|
||||||
polices []*Policy
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
//别名拼接 r.key m.permissions
|
//别名拼接 r.key m.permissions
|
||||||
q := func(alias string, column string) string {
|
q := func(alias string, column string) string {
|
||||||
@@ -113,23 +127,43 @@ func loadPermissions(ctx context.Context) {
|
|||||||
Where(q("m", dao.AdminMenu.Columns().Status), consts.StatusEnabled).
|
Where(q("m", dao.AdminMenu.Columns().Status), consts.StatusEnabled).
|
||||||
WhereNot(q("m", dao.AdminMenu.Columns().Permissions), "").
|
WhereNot(q("m", dao.AdminMenu.Columns().Permissions), "").
|
||||||
WhereNot(q("r", dao.AdminRole.Columns().Key), consts.SuperRoleKey).
|
WhereNot(q("r", dao.AdminRole.Columns().Key), consts.SuperRoleKey).
|
||||||
Scan(&polices)
|
Scan(&records)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
if strings.Contains(record.Permissions, ",") {
|
||||||
|
lst := strings.Split(record.Permissions, ",")
|
||||||
|
for _, permission := range lst {
|
||||||
|
permission = strings.TrimSpace(permission)
|
||||||
|
if permission == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
policies = append(policies, &PermissionPolicy{Key: record.Key, Perm: permission, ActRegexp: ActionAll})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
permission := strings.TrimSpace(record.Permissions)
|
||||||
|
if permission == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
policies = append(policies, &PermissionPolicy{Key: record.Key, Perm: permission, ActRegexp: ActionAll})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPermissions(ctx context.Context) {
|
||||||
|
policies, err := queryPermissionPolicies(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Fatalf(ctx, "loadPermissions Scan err:%v", err)
|
g.Log().Fatalf(ctx, "loadPermissions Scan err:%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, policy := range polices {
|
rules := make([][]string, 0, len(policies))
|
||||||
if strings.Contains(policy.Permissions, ",") {
|
for _, policy := range policies {
|
||||||
lst := strings.Split(policy.Permissions, ",")
|
rules = append(rules, []string{policy.Key, policy.Perm, policy.ActRegexp})
|
||||||
for _, permissions := range lst {
|
|
||||||
rules = append(rules, []string{policy.Key, permissions, ActionAll})
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
rules = append(rules, []string{policy.Key, policy.Permissions, ActionAll})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = Enforcer.AddPolicies(rules); err != nil {
|
if _, err = Enforcer.AddPolicies(rules); err != nil {
|
||||||
g.Log().Fatalf(ctx, "loadPermissions AddPolicies err:%v", err)
|
g.Log().Fatalf(ctx, "loadPermissions AddPolicies err:%v", err)
|
||||||
return
|
return
|
||||||
@@ -160,5 +194,65 @@ func Refresh(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
loadPermissions(ctx)
|
loadPermissions(ctx)
|
||||||
|
return RefreshPermissionCache(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshPermissionCache(ctx context.Context) (err error) {
|
||||||
|
policies, err := queryPermissionPolicies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rolePolicies := make(map[string][]*PermissionPolicy)
|
||||||
|
for _, policy := range policies {
|
||||||
|
rolePolicies[policy.Key] = append(rolePolicies[policy.Key], policy)
|
||||||
|
}
|
||||||
|
data, err := gjson.Encode(rolePolicies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cache.Instance().Set(ctx, permissionCacheKey, string(data), 30*24*time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachedPermissionPolicies(ctx context.Context) (rolePolicies map[string][]*PermissionPolicy, err error) {
|
||||||
|
value, err := cache.Instance().Get(ctx, permissionCacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value.IsNil() {
|
||||||
|
if err = RefreshPermissionCache(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value, err = cache.Instance().Get(ctx, permissionCacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rolePolicies = make(map[string][]*PermissionPolicy)
|
||||||
|
if value.IsNil() {
|
||||||
|
return rolePolicies, nil
|
||||||
|
}
|
||||||
|
if err = gjson.DecodeTo([]byte(value.String()), &rolePolicies); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Verify(ctx context.Context, roleKey, path, method string) (bool, error) {
|
||||||
|
rolePolicies, err := cachedPermissionPolicies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, policy := range rolePolicies[roleKey] {
|
||||||
|
if !casbinutil.KeyMatch(path, policy.Perm) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok, err := regexp.MatchString(policy.ActRegexp, method)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,11 @@ import (
|
|||||||
var tenantDbGroups sync.Map
|
var tenantDbGroups sync.Map
|
||||||
|
|
||||||
const registryDbGroup = "imRegistry"
|
const registryDbGroup = "imRegistry"
|
||||||
|
const sharedBusinessDatabase = "meeting_im"
|
||||||
|
|
||||||
var registryDbGroupOnce sync.Once
|
var registryDbGroupOnce sync.Once
|
||||||
|
|
||||||
// DB resolves a tenant registry record and returns the corresponding business DB.
|
// DB resolves a tenant registry record and returns the shared IM business DB.
|
||||||
func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
|
func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
|
||||||
if tenantId <= 0 {
|
if tenantId <= 0 {
|
||||||
return nil, nil, gerror.New("租户ID不能为空")
|
return nil, nil, gerror.New("租户ID不能为空")
|
||||||
@@ -36,27 +37,44 @@ func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant
|
|||||||
return nil, nil, gerror.New("租户不存在")
|
return nil, nil, gerror.New("租户不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := tenantLink(ctx, tenant)
|
db, err = SharedDB(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
return db, tenant, nil
|
||||||
|
}
|
||||||
|
|
||||||
group := fmt.Sprintf("im_tenant_%d_%x", tenant.Id, hash(link))
|
// SharedDB returns the common IM database configured by the default tenant.
|
||||||
|
// Tenant isolation inside this database is implemented by tenant_id predicates
|
||||||
|
// and the three tenant-partitioned high-volume table families.
|
||||||
|
func SharedDB(ctx context.Context) (gdb.DB, error) {
|
||||||
|
defaultLink := defaultDbLink(ctx)
|
||||||
|
if defaultLink == "" {
|
||||||
|
return nil, gerror.New("默认数据库连接未配置,无法连接共享业务库")
|
||||||
|
}
|
||||||
|
link, err := replaceMySQLDatabase(defaultLink, sharedBusinessDatabase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dbForLink(link, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbForLink(link string, ownerID int64) (gdb.DB, error) {
|
||||||
|
group := fmt.Sprintf("im_shared_%d_%x", ownerID, hash(link))
|
||||||
if cached, ok := tenantDbGroups.Load(group); ok && cached == link {
|
if cached, ok := tenantDbGroups.Load(group); ok && cached == link {
|
||||||
db, err = gdb.Instance(group)
|
return gdb.Instance(group)
|
||||||
return db, tenant, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
|
if err := gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
|
||||||
return nil, nil, gerror.Wrap(err, "初始化租户业务库连接失败")
|
return nil, gerror.Wrap(err, "初始化共享业务库连接失败")
|
||||||
}
|
}
|
||||||
tenantDbGroups.Store(group, link)
|
tenantDbGroups.Store(group, link)
|
||||||
|
|
||||||
db, err = gdb.Instance(group)
|
db, err := gdb.Instance(group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, gerror.Wrap(err, "获取租户业务库连接失败")
|
return nil, gerror.Wrap(err, "获取共享业务库连接失败")
|
||||||
}
|
}
|
||||||
return db, tenant, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryDB returns the dedicated tenant registry connection, or the default
|
// RegistryDB returns the dedicated tenant registry connection, or the default
|
||||||
@@ -90,31 +108,6 @@ func ensureRegistryDBGroup() {
|
|||||||
_ = gdb.SetConfigGroup(registryDbGroup, cloned)
|
_ = gdb.SetConfigGroup(registryDbGroup, cloned)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
|
|
||||||
if tenant.DbDsn != "" {
|
|
||||||
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
|
||||||
}
|
|
||||||
if tenant.DbName == "" {
|
|
||||||
// 未配置独立业务库的商户回落共享默认库
|
|
||||||
defaultLink := defaultDbLink(ctx)
|
|
||||||
if defaultLink == "" {
|
|
||||||
return "", gerror.New("默认数据库连接未配置,无法回落共享业务库")
|
|
||||||
}
|
|
||||||
return normalizeMySQLTimeOptions(defaultLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultLink := defaultDbLink(ctx)
|
|
||||||
if defaultLink == "" {
|
|
||||||
return "", gerror.New("默认数据库连接未配置,无法推导租户业务库连接")
|
|
||||||
}
|
|
||||||
|
|
||||||
link, err := replaceMySQLDatabase(defaultLink, tenant.DbName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultDbLink(ctx context.Context) string {
|
func defaultDbLink(ctx context.Context) string {
|
||||||
if configs, err := gdb.GetConfigGroup("default"); err == nil && len(configs) > 0 {
|
if configs, err := gdb.GetConfigGroup("default"); err == nil && len(configs) > 0 {
|
||||||
if configs[0].Link != "" {
|
if configs[0].Link != "" {
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeMySQLTimeOptions(t *testing.T) {
|
func TestNormalizeMySQLTimeOptions(t *testing.T) {
|
||||||
link, err := normalizeMySQLTimeOptions("user:pass@tcp(127.0.0.1:3306)/tinode?charset=utf8mb4")
|
link, err := normalizeMySQLTimeOptions("user:pass@tcp(127.0.0.1:3306)/meeting_im?charset=utf8mb4")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("normalize raw mysql dsn: %v", err)
|
t.Fatalf("normalize raw mysql dsn: %v", err)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(link, "mysql:user:pass@tcp(127.0.0.1:3306)/tinode?") {
|
if !strings.HasPrefix(link, "mysql:user:pass@tcp(127.0.0.1:3306)/meeting_im?") {
|
||||||
t.Fatalf("unexpected normalized link: %s", link)
|
t.Fatalf("unexpected normalized link: %s", link)
|
||||||
}
|
}
|
||||||
if !strings.Contains(link, "loc=UTC") || !strings.Contains(link, "parseTime=true") {
|
if !strings.Contains(link, "loc=UTC") || !strings.Contains(link, "parseTime=true") {
|
||||||
@@ -20,17 +20,17 @@ func TestNormalizeMySQLTimeOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeMySQLTimeOptionsRejectsUnsupportedFormat(t *testing.T) {
|
func TestNormalizeMySQLTimeOptionsRejectsUnsupportedFormat(t *testing.T) {
|
||||||
if _, err := normalizeMySQLTimeOptions("postgres://user:pass@127.0.0.1:5432/tinode"); err == nil {
|
if _, err := normalizeMySQLTimeOptions("postgres://user:pass@127.0.0.1:5432/meeting_im"); err == nil {
|
||||||
t.Fatal("expected unsupported format error")
|
t.Fatal("expected unsupported format error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) {
|
func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) {
|
||||||
link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "tinode")
|
link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "meeting_im")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, expected := range []string{"/tinode?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} {
|
for _, expected := range []string{"/meeting_im?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} {
|
||||||
if !strings.Contains(link, expected) {
|
if !strings.Contains(link, expected) {
|
||||||
t.Fatalf("link %q does not contain %q", link, expected)
|
t.Fatalf("link %q does not contain %q", link, expected)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,27 +65,41 @@ func DefaultClient(ctx context.Context) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Get(ctx context.Context, path string, values url.Values, out any) error {
|
func (c *Client) Get(ctx context.Context, path string, values url.Values, out any) error {
|
||||||
|
return c.GetForTenant(ctx, path, values, 0, "", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetForTenant(ctx context.Context, path string, values url.Values, tenantID int64, tenantCode string, out any) error {
|
||||||
if values != nil {
|
if values != nil {
|
||||||
if query := values.Encode(); query != "" {
|
if query := values.Encode(); query != "" {
|
||||||
path += "?" + query
|
path += "?" + query
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.doJSON(ctx, http.MethodGet, path, nil, out)
|
return c.doJSON(ctx, http.MethodGet, path, nil, tenantID, tenantCode, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Post(ctx context.Context, path string, in any, out any) error {
|
func (c *Client) Post(ctx context.Context, path string, in any, out any) error {
|
||||||
|
return c.PostForTenant(ctx, path, in, 0, "", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostForTenant(ctx context.Context, path string, in any, tenantID int64, tenantCode string, out any) error {
|
||||||
body, err := json.Marshal(in)
|
body, err := json.Marshal(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "序列化工作台请求失败")
|
return gerror.Wrap(err, "序列化工作台请求失败")
|
||||||
}
|
}
|
||||||
return c.doJSON(ctx, http.MethodPost, path, body, out)
|
return c.doJSON(ctx, http.MethodPost, path, body, tenantID, tenantCode, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, out any) error {
|
func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, tenantID int64, tenantCode string, out any) error {
|
||||||
req, err := c.newRequest(ctx, method, path, body)
|
req, err := c.newRequest(ctx, method, path, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if tenantID > 0 {
|
||||||
|
req.Header.Set("X-Tenant-ID", strconv.FormatInt(tenantID, 10))
|
||||||
|
}
|
||||||
|
if tenantCode = strings.TrimSpace(tenantCode); tenantCode != "" {
|
||||||
|
req.Header.Set("X-Tenant-Code", tenantCode)
|
||||||
|
}
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Warningf(ctx, "IM workbench request failed url:%s err:%+v", req.URL.String(), err)
|
g.Log().Warningf(ctx, "IM workbench request failed url:%s err:%+v", req.URL.String(), err)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package imworkbench
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostForTenantAddsTenantHeaders(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got := r.Header.Get("X-Tenant-ID"); got != "23" {
|
||||||
|
t.Fatalf("X-Tenant-ID = %q, want 23", got)
|
||||||
|
}
|
||||||
|
if got := r.Header.Get("X-Tenant-Code"); got != "merchant-23" {
|
||||||
|
t.Fatalf("X-Tenant-Code = %q, want merchant-23", got)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":0,"data":{"tenant_id":23}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
conf: Config{BaseURL: server.URL, ServiceName: "test", Secret: "secret", Timeout: time.Second},
|
||||||
|
http: server.Client(),
|
||||||
|
}
|
||||||
|
var response struct {
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
}
|
||||||
|
if err := client.PostForTenant(context.Background(), "/initialize", map[string]any{"tenant_id": 23}, 23, "merchant-23", &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if response.TenantID != 23 {
|
||||||
|
t.Fatalf("tenant_id = %d, want 23", response.TenantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
// Package maoyuadmin 是 im_manager 调用毛隅手记服务(maoyu_server)内部管理接口的客户端。
|
||||||
|
//
|
||||||
|
// 与 cultivateadmin 相互独立:路径前缀固定为 /internal/admin/maoyu/tenants/{tenant_code},
|
||||||
|
// 配置段为 maoyuAdminApi,密钥优先使用 maoyuAdminApi.serviceSecret,
|
||||||
|
// 未配置时回落到与 imInternalApi 共享的密钥。
|
||||||
|
package maoyuadmin
|
||||||
|
|
||||||
|
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://maoyu-server.maoyu-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, "maoyuAdminApi.timeout", "5s").Duration()
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 5 * time.Second
|
||||||
|
}
|
||||||
|
baseURL := resolveValue(g.Cfg().MustGet(ctx, "maoyuAdminApi.baseUrl", defaultBaseURL).String())
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = defaultBaseURL
|
||||||
|
}
|
||||||
|
secret := firstValue(
|
||||||
|
g.Cfg().MustGet(ctx, "maoyuAdminApi.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, "maoyuAdminApi.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, "Maoyu 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, "Maoyu 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 maoyuError(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/maoyu/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 maoyuError(ctx context.Context, status int, reason, message, requestID string) error {
|
||||||
|
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
||||||
|
g.Log().Warningf(ctx, "Maoyu admin auth failed status:%d reason:%s requestId:%s", status, reason, requestID)
|
||||||
|
return gerror.New("毛隅手记服务内部鉴权失败")
|
||||||
|
}
|
||||||
|
if status >= http.StatusInternalServerError {
|
||||||
|
g.Log().Warningf(ctx, "Maoyu 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,84 @@
|
|||||||
|
package maoyuadmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientSignsScopeAndTargetsMaoyuPrefix(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/maoyu/tenants/default/content-reviews?status=pending" {
|
||||||
|
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{"post_count": 7, "pending_audit_count": 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 {
|
||||||
|
PostCount int64 `json:"postCount"`
|
||||||
|
PendingAuditCount int64 `json:"pendingAuditCount"`
|
||||||
|
}
|
||||||
|
err := client.Get(context.Background(), Scope{TenantID: 1, TenantCode: "default", OperatorID: 14, RoleKey: "imManager", RequestID: "req-test"}, "/content-reviews", url.Values{"status": []string{"pending"}}, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out.PostCount != 7 || out.PendingAuditCount != 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -470,9 +470,10 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
|||||||
|
|
||||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||||
if in.TenantId > 0 {
|
if in.TenantId > 0 {
|
||||||
|
// provisioning(初始化中)是新建商户创建管理员前的合法过渡态,必须放行,否则新建商户必失败
|
||||||
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||||
Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive).
|
WhereIn(dao.ImTenant.Columns().State, g.Slice{consts.ImTenantStateActive, consts.ImTenantStateProvisioning}).
|
||||||
Count()
|
Count()
|
||||||
if queryErr != nil {
|
if queryErr != nil {
|
||||||
return gerror.Wrap(queryErr, "校验IM租户失败")
|
return gerror.Wrap(queryErr, "校验IM租户失败")
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ func (s *sAdminMenu) Delete(ctx context.Context, in *adminin.MenuDeleteInp) (err
|
|||||||
if !exist.IsEmpty() {
|
if !exist.IsEmpty() {
|
||||||
return gerror.New("请先删除该菜单下的所有菜单!")
|
return gerror.New("请先删除该菜单下的所有菜单!")
|
||||||
}
|
}
|
||||||
_, err = dao.AdminMenu.Ctx(ctx).Where(dao.AdminMenu.Columns().Id, in.Id).Delete()
|
if _, err = dao.AdminMenu.Ctx(ctx).Where(dao.AdminMenu.Columns().Id, in.Id).Delete(); err != nil {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
return casbin.Refresh(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUnique 验证菜单唯一属性
|
// VerifyUnique 验证菜单唯一属性
|
||||||
@@ -104,7 +106,7 @@ func (s *sAdminMenu) Edit(ctx context.Context, in *adminin.MenuEditInp) (err err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
if err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||||
in.Pid, in.Level, in.Tree, err = hgorm.AutoUpdateTree(ctx, &dao.AdminMenu, in.Id, in.Pid)
|
in.Pid, in.Level, in.Tree, err = hgorm.AutoUpdateTree(ctx, &dao.AdminMenu, in.Id, in.Pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -121,8 +123,11 @@ func (s *sAdminMenu) Edit(ctx context.Context, in *adminin.MenuEditInp) (err err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return casbin.Refresh(ctx)
|
return casbin.Refresh(ctx)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List 获取菜单列表
|
// List 获取菜单列表
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := casbin.Enforcer.Enforce(user.RoleKey, path, method)
|
ok, err := casbin.Verify(ctx, user.RoleKey, path, method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Infof(ctx, "admin Verify Enforce err:%+v", err)
|
g.Log().Infof(ctx, "admin Verify cache err:%+v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
@@ -221,15 +221,18 @@ func (s *sAdminRole) Edit(ctx context.Context, in *adminin.RoleEditInp) (err err
|
|||||||
// 如果当前角色有子级,更新子级tree关系树
|
// 如果当前角色有子级,更新子级tree关系树
|
||||||
return updateRoleChildrenTree(ctx, in.Id, in.Level, in.Tree)
|
return updateRoleChildrenTree(ctx, in.Id, in.Level, in.Tree)
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
return casbin.Refresh(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// 新增
|
// 新增
|
||||||
if _, err = dao.AdminRole.Ctx(ctx).Fields(adminin.RoleInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
|
if _, err = dao.AdminRole.Ctx(ctx).Fields(adminin.RoleInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return casbin.Refresh(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateRoleChildrenTree(ctx context.Context, _id int64, _level int, _tree string) (err error) {
|
func updateRoleChildrenTree(ctx context.Context, _id int64, _level int, _tree string) (err error) {
|
||||||
@@ -288,8 +291,9 @@ func (s *sAdminRole) Delete(ctx context.Context, in *adminin.RoleDeleteInp) (err
|
|||||||
|
|
||||||
if _, err = dao.AdminRole.Ctx(ctx).Where("id", in.Id).Delete(); err != nil {
|
if _, err = dao.AdminRole.Ctx(ctx).Where("id", in.Id).Delete(); err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
}
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
return casbin.Refresh(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sAdminRole) DataScopeSelect() (res form.Selects) {
|
func (s *sAdminRole) DataScopeSelect() (res form.Selects) {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ func (s *sHook) setAdminStaticCacheHeader(r *ghttp.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, "/admin/assets/") {
|
if strings.HasPrefix(path, "/admin/assets/") {
|
||||||
r.Response.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
r.Response.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
r.Response.Header().Set("Pragma", "no-cache")
|
||||||
|
r.Response.Header().Set("Expires", "0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ func (s *sMiddleware) AdminAuth(r *ghttp.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已绑定 IM 租户的后台账号需要读取自己的租户选项,用于各 IM 页面初始化租户下拉。
|
||||||
|
// 业务层会继续按登录态 tenant_id 收敛查询范围,这里只绕过菜单权限,不扩大数据范围。
|
||||||
|
if path == "/imTenant/list" && r.Method == "GET" && contexts.GetTenantId(ctx) > 0 {
|
||||||
|
r.Middleware.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 验证路由访问权限
|
// 验证路由访问权限
|
||||||
if !service.AdminRole().Verify(ctx, path, r.Method) {
|
if !service.AdminRole().Verify(ctx, path, r.Method) {
|
||||||
g.Log().Debugf(ctx, "AdminAuth fail path:%+v, GetRoleKey:%+v, r.Method:%+v", path, contexts.GetRoleKey(ctx), r.Method)
|
g.Log().Debugf(ctx, "AdminAuth fail path:%+v, GetRoleKey:%+v, r.Method:%+v", path, contexts.GetRoleKey(ctx), r.Method)
|
||||||
|
|||||||
@@ -2,25 +2,10 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"hotgo/internal/library/cultivateadmin"
|
||||||
"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/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
)
|
)
|
||||||
@@ -30,709 +15,167 @@ type sSysCultivateContent struct{}
|
|||||||
func NewSysCultivateContent() *sSysCultivateContent { return &sSysCultivateContent{} }
|
func NewSysCultivateContent() *sSysCultivateContent { return &sSysCultivateContent{} }
|
||||||
func init() { service.RegisterSysCultivateContent(NewSysCultivateContent()) }
|
func init() { service.RegisterSysCultivateContent(NewSysCultivateContent()) }
|
||||||
|
|
||||||
func (s *sSysCultivateContent) Dashboard(ctx context.Context, _ *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) {
|
func (s *sSysCultivateContent) Dashboard(ctx context.Context, in *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) {
|
||||||
tenantID := cultivateTenantID(ctx)
|
if err := in.Filter(ctx); err != nil {
|
||||||
out := new(sysin.CultivateDashboardModel)
|
return nil, err
|
||||||
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}},
|
|
||||||
}
|
}
|
||||||
for _, q := range queries {
|
scope, err := cultivateAdminScope(ctx)
|
||||||
value, err := db.GetValue(ctx, q.sql, q.args...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "获取养成统计失败")
|
return nil, err
|
||||||
}
|
}
|
||||||
*q.target = value.Int64()
|
result := new(sysin.CultivateDashboardModel)
|
||||||
|
if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, "/dashboard", nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return out, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) {
|
func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
tenantID := cultivateTenantID(ctx)
|
scope, err := cultivateAdminScope(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 {
|
if err != nil {
|
||||||
return nil, 0, gerror.Wrap(err, "获取评论审核列表失败")
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
list = make([]*sysin.CultivateContentListModel, 0, len(rows))
|
normalizeCultivatePage(&in.PageReq)
|
||||||
for _, row := range rows {
|
values := cultivatePageValues(in.Page, in.PerPage)
|
||||||
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})
|
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{}
|
||||||
}
|
}
|
||||||
columns := dao.FeedPost.Columns()
|
return page.Items, page.Total, nil
|
||||||
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)
|
|
||||||
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_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})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) {
|
func (s *sSysCultivateContent) View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tenantID := cultivateTenantID(ctx)
|
scope, err := cultivateAdminScope(ctx)
|
||||||
base := &sysin.CultivateContentViewModel{Media: []*sysin.CultivateMediaModel{}, Audits: []*sysin.CultivateAuditModel{}, RelatedReports: []*sysin.CultivateReportListModel{}}
|
if err != nil {
|
||||||
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if row.Id == 0 {
|
result := new(sysin.CultivateContentViewModel)
|
||||||
return nil, gerror.New("评论不存在")
|
path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id)
|
||||||
}
|
if err = cultivateadmin.DefaultClient(ctx).Get(ctx, scope, path, nil, result); err != nil {
|
||||||
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if row.Id == 0 {
|
return result, nil
|
||||||
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) {
|
|
||||||
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})
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error {
|
func (s *sSysCultivateContent) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx)
|
scope, err := cultivateAdminScope(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)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if authorID > 0 {
|
path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id) + "/actions"
|
||||||
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 cultivateadmin.DefaultClient(ctx).Post(ctx, scope, path, map[string]any{"action": in.Action, "reason": strings.TrimSpace(in.Reason)}, nil)
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) {
|
func (s *sSysCultivateContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := &sysin.CultivateContentBatchReviewModel{Failures: make(map[string]string)}
|
scope, err := cultivateAdminScope(ctx)
|
||||||
for _, id := range in.Ids {
|
if err != nil {
|
||||||
item := &sysin.CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason}
|
return nil, err
|
||||||
if err := item.Filter(ctx); err != nil {
|
|
||||||
result.FailedCount++
|
|
||||||
result.Failures[id] = err.Error()
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if err := s.Review(ctx, item); err != nil {
|
result := new(sysin.CultivateContentBatchReviewModel)
|
||||||
result.FailedCount++
|
err = cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/content-reviews/batch-actions", map[string]any{
|
||||||
result.Failures[id] = err.Error()
|
"object_type": in.ObjectType, "ids": in.Ids, "action": in.Action, "reason": strings.TrimSpace(in.Reason),
|
||||||
continue
|
}, result)
|
||||||
}
|
if err != nil {
|
||||||
result.SuccessCount++
|
return nil, err
|
||||||
}
|
}
|
||||||
return result, nil
|
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) {
|
func (s *sSysCultivateContent) ReportList(ctx context.Context, in *sysin.CultivateReportListInp) (list []*sysin.CultivateReportListModel, totalCount int, err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
columns := dao.FeedReport.Columns()
|
scope, err := cultivateAdminScope(ctx)
|
||||||
mod := dao.FeedReport.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx))
|
if err != nil {
|
||||||
if in.ObjectType != "" {
|
return nil, 0, err
|
||||||
mod = mod.Where(columns.ObjectType, in.ObjectType)
|
|
||||||
}
|
}
|
||||||
if in.Status != "" {
|
normalizeCultivatePage(&in.PageReq)
|
||||||
mod = mod.Where(columns.Status, in.Status)
|
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 != "" {
|
if page.Items == nil {
|
||||||
mod = mod.Where(columns.ReporterId, in.ReporterId)
|
page.Items = []*sysin.CultivateReportListModel{}
|
||||||
}
|
}
|
||||||
if len(in.CreatedAt) == 2 {
|
return page.Items, page.Total, nil
|
||||||
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}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) {
|
func (s *sSysCultivateContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tenantID := cultivateTenantID(ctx)
|
scope, err := cultivateAdminScope(ctx)
|
||||||
c := dao.FeedReport.Columns()
|
if err != nil {
|
||||||
var row entity.FeedReport
|
|
||||||
if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.Id: in.ReportId, c.TenantId: tenantID}).Scan(&row); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if row.Id == 0 {
|
result := new(sysin.CultivateReportViewModel)
|
||||||
return nil, gerror.New("举报记录不存在")
|
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{}}
|
return result, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error {
|
func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx)
|
scope, err := cultivateAdminScope(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)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if in.Action == "resolve" || in.Action == "reject" {
|
return cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/reports/"+url.PathEscape(in.Id)+"/actions", map[string]any{
|
||||||
summary := "你的举报已处理,感谢你帮助维护社区环境"
|
"action": in.Action, "handle_result": in.HandleResult, "content_action": in.ContentAction,
|
||||||
if in.Action == "reject" {
|
"user_action": in.UserAction, "reason": in.Reason,
|
||||||
summary = "你的举报已完成核查,暂未发现违规"
|
}, nil)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) {
|
func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) {
|
||||||
columns := dao.AdminOperationLog.Columns()
|
if err = in.Filter(ctx); err != nil {
|
||||||
mod := dao.AdminOperationLog.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx))
|
return nil, 0, err
|
||||||
if in.OperatorId != "" {
|
|
||||||
mod = mod.Where(columns.OperatorId, in.OperatorId)
|
|
||||||
}
|
}
|
||||||
if in.OperationType != "" {
|
scope, err := cultivateAdminScope(ctx)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
var out struct {
|
normalizeCultivatePage(&in.PageReq)
|
||||||
Items []*cultivateProfile `json:"items"`
|
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 page.Items == nil {
|
||||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_ids": ids}, &out); err != nil {
|
page.Items = []*sysin.CultivateOperationLogListModel{}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
for _, item := range out.Items {
|
return page.Items, page.Total, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,20 @@ package sys
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"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/gctx"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
|
||||||
"hotgo/internal/dao"
|
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
"hotgo/internal/library/imadmin"
|
"hotgo/internal/library/cultivateadmin"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/model/input/form"
|
||||||
"hotgo/internal/library/location"
|
|
||||||
"hotgo/internal/model/entity"
|
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
)
|
)
|
||||||
@@ -30,6 +26,40 @@ type sSysCultivateFlower struct{}
|
|||||||
func NewSysCultivateFlower() *sSysCultivateFlower { return &sSysCultivateFlower{} }
|
func NewSysCultivateFlower() *sSysCultivateFlower { return &sSysCultivateFlower{} }
|
||||||
func init() { service.RegisterSysCultivateFlower(NewSysCultivateFlower()) }
|
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"`
|
||||||
|
WaitHours uint `json:"waitHours"`
|
||||||
|
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 {
|
func cultivateTenantID(ctx context.Context) int64 {
|
||||||
id := contexts.GetTenantId(ctx)
|
id := contexts.GetTenantId(ctx)
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
@@ -38,222 +68,212 @@ func cultivateTenantID(ctx context.Context) int64 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) {
|
func cultivateAdminScope(ctx context.Context) (cultivateadmin.Scope, error) {
|
||||||
c := dao.FlowerCatalog.Columns()
|
tenant, err := resolveImAdminTenant(ctx, cultivateTenantID(ctx))
|
||||||
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 err != nil {
|
||||||
if in.FlowerCode != "" {
|
return cultivateadmin.Scope{}, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
operatorID := contexts.GetUserId(ctx)
|
operatorID := contexts.GetUserId(ctx)
|
||||||
now := time.Now().UTC()
|
if operatorID <= 0 {
|
||||||
version := "cat_" + now.Format("20060102_150405")
|
return cultivateadmin.Scope{}, gerror.New("当前管理员身份无效")
|
||||||
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 cultivateadmin.Scope{
|
||||||
return gerror.New("花种不存在")
|
TenantID: tenant.Id, TenantCode: tenant.Code, OperatorID: operatorID,
|
||||||
}
|
RoleKey: contexts.GetRoleKey(ctx), RequestID: gctx.CtxId(ctx),
|
||||||
if before.FlowerCode != in.FlowerCode {
|
}, nil
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCultivateAssets(ctx context.Context, tenantID int64, in *sysin.CultivateFlowerEditInp) error {
|
func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) {
|
||||||
seen := make(map[string]struct{})
|
if err = in.Filter(ctx); err != nil {
|
||||||
fileIDs := make([]string, 0, 1+len(in.StageRules)*2)
|
return nil, 0, err
|
||||||
add := func(fileID string) {
|
|
||||||
fileID = strings.TrimSpace(fileID)
|
|
||||||
if fileID == "" {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if _, ok := seen[fileID]; ok {
|
scope, err := cultivateAdminScope(ctx)
|
||||||
return
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
seen[fileID] = struct{}{}
|
normalizeCultivatePage(&in.PageReq)
|
||||||
fileIDs = append(fileIDs, fileID)
|
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
|
||||||
}
|
}
|
||||||
add(in.IconFileId)
|
list = make([]*sysin.CultivateFlowerListModel, 0, len(page.Items))
|
||||||
for _, rule := range in.StageRules {
|
for index := range page.Items {
|
||||||
add(rule.ImageFileId)
|
item, convertErr := flowerListModel(&page.Items[index])
|
||||||
add(rule.AnimationFileId)
|
if convertErr != nil {
|
||||||
|
return nil, 0, convertErr
|
||||||
}
|
}
|
||||||
if len(fileIDs) == 0 {
|
list = append(list, item)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
tenant, err := resolveImAdminTenant(ctx, tenantID)
|
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
|
||||||
|
}
|
||||||
|
scope, err := cultivateAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/files/validate"
|
payload := flowerPayload(in)
|
||||||
for offset := 0; offset < len(fileIDs); offset += 20 {
|
var response struct {
|
||||||
end := offset + 20
|
ID string `json:"id"`
|
||||||
if end > len(fileIDs) {
|
|
||||||
end = len(fileIDs)
|
|
||||||
}
|
}
|
||||||
var out struct {
|
if in.Id == 0 {
|
||||||
Items []struct {
|
err = cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/flowers", payload, &response)
|
||||||
FileId string `json:"file_id"`
|
} else {
|
||||||
ReviewStatus string `json:"review_status"`
|
err = cultivateadmin.DefaultClient(ctx).Put(ctx, scope, "/flowers/"+strconv.FormatUint(in.Id, 10), payload, &response)
|
||||||
} `json:"items"`
|
|
||||||
}
|
}
|
||||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_id": 0, "file_ids": fileIDs[offset:end]}, &out); err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "校验花种素材失败")
|
return 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)
|
|
||||||
}
|
}
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error {
|
func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error {
|
||||||
tenantID := cultivateTenantID(ctx)
|
if err := in.Filter(ctx); err != nil {
|
||||||
operatorID := contexts.GetUserId(ctx)
|
return err
|
||||||
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 {
|
scope, err := cultivateAdminScope(ctx)
|
||||||
return gerror.New("花种不存在")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
data := g.Map{c.Status: in.Status, c.ConfigVersion: "cat_" + now.Format("20060102_150405"), c.UpdatedAt: now}
|
return cultivateadmin.DefaultClient(ctx).Post(ctx, scope, "/flowers/"+strconv.FormatUint(in.Id, 10)+"/status", map[string]any{
|
||||||
if in.Status == "online" {
|
"status": in.Status, "reason": strings.TrimSpace(in.Reason),
|
||||||
data[c.OnlineAt] = now
|
}, nil)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeCultivateOperation(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, operation, targetType, targetID string, before, after any, reason string, now time.Time) error {
|
func flowerPayload(in *sysin.CultivateFlowerEditInp) map[string]any {
|
||||||
id, e := imtenant.NextSnowflakeID(ctx)
|
shareConfig := map[string]any{}
|
||||||
if e != nil {
|
if in.ShareConfig != nil {
|
||||||
return e
|
_ = json.Unmarshal([]byte(in.ShareConfig.MustToJsonString()), &shareConfig)
|
||||||
}
|
}
|
||||||
beforeJSON, _ := json.Marshal(before)
|
rules := make([]map[string]any, 0, len(in.StageRules))
|
||||||
afterJSON, _ := json.Marshal(after)
|
for _, rule := range in.StageRules {
|
||||||
ipAddress, userAgent := "", ""
|
if rule == nil {
|
||||||
if request := ghttp.RequestFromCtx(ctx); request != nil {
|
continue
|
||||||
ipAddress = location.GetClientIp(request)
|
}
|
||||||
userAgent = request.UserAgent()
|
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,
|
||||||
|
"wait_hours": rule.WaitHours, "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, WaitHours: rule.WaitHours, 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,16 @@ func (s *sSysImAppRelease) List(ctx context.Context, in *sysin.ImAppReleaseListI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
mod := s.model(ctx, db)
|
mod := s.model(ctx, db)
|
||||||
|
if in.AppCode != "" {
|
||||||
|
mod = mod.Where("app_code", in.AppCode)
|
||||||
|
}
|
||||||
if in.Platform != "" {
|
if in.Platform != "" {
|
||||||
mod = mod.Where("platform", in.Platform)
|
mod = mod.Where("platform", in.Platform)
|
||||||
}
|
}
|
||||||
if in.Channel != "" {
|
if in.Channel != "" {
|
||||||
mod = mod.WhereLike("channel", "%"+in.Channel+"%")
|
mod = mod.WhereLike("channel", "%"+in.Channel+"%")
|
||||||
}
|
}
|
||||||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("platform").OrderAsc("channel")
|
mod = mod.Page(in.Page, in.PerPage).OrderAsc("app_code").OrderAsc("platform").OrderAsc("channel")
|
||||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||||
err = gerror.Wrap(err, "获取IM客户端版本策略失败,请稍后重试!")
|
err = gerror.Wrap(err, "获取IM客户端版本策略失败,请稍后重试!")
|
||||||
return
|
return
|
||||||
@@ -59,6 +62,7 @@ func (s *sSysImAppRelease) Edit(ctx context.Context, in *sysin.ImAppReleaseEditI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := g.Map{
|
data := g.Map{
|
||||||
|
"app_code": in.AppCode,
|
||||||
"platform": in.Platform,
|
"platform": in.Platform,
|
||||||
"channel": in.Channel,
|
"channel": in.Channel,
|
||||||
"latest_version": in.LatestVersion,
|
"latest_version": in.LatestVersion,
|
||||||
@@ -85,18 +89,18 @@ func (s *sSysImAppRelease) Edit(ctx context.Context, in *sysin.ImAppReleaseEditI
|
|||||||
return gerror.Wrap(err, "新增IM客户端版本策略失败,请稍后重试!")
|
return gerror.Wrap(err, "新增IM客户端版本策略失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
idValue, err := db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields("id").
|
idValue, err := db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields("id").
|
||||||
Where("platform", in.Platform).Where("channel", in.Channel).Value()
|
Where("app_code", in.AppCode).Where("platform", in.Platform).Where("channel", in.Channel).Value()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "获取IM客户端版本策略ID失败,请稍后重试!")
|
return gerror.Wrap(err, "获取IM客户端版本策略ID失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
targetId = idValue.Int64()
|
targetId = idValue.Int64()
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, 0, action, "im_app_release", targetId, "success", g.Map{
|
_ = writeImAdminAudit(ctx, 0, action, "im_app_release", targetId, "success", g.Map{
|
||||||
"platform": in.Platform, "channel": in.Channel, "update_type": in.UpdateType,
|
"app_code": in.AppCode, "platform": in.Platform, "channel": in.Channel, "update_type": in.UpdateType,
|
||||||
})
|
})
|
||||||
if err = imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
if err = imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
||||||
TenantCode: "*", EventType: "app.release.updated", Group: "app_release",
|
TenantCode: "*", EventType: "app.release.updated", Group: "app_release",
|
||||||
Keys: []string{in.Platform, in.Channel}, BizID: in.Platform + ":" + in.Channel,
|
Keys: []string{in.AppCode, in.Platform, in.Channel}, BizID: in.AppCode + ":" + in.Platform + ":" + in.Channel,
|
||||||
Actor: &imkafka.Actor{Type: "admin", ID: fmt.Sprint(contexts.GetUserId(ctx))},
|
Actor: &imkafka.Actor{Type: "admin", ID: fmt.Sprint(contexts.GetUserId(ctx))},
|
||||||
CreatedBy: contexts.GetUserId(ctx),
|
CreatedBy: contexts.GetUserId(ctx),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -108,6 +112,7 @@ func (s *sSysImAppRelease) Edit(ctx context.Context, in *sysin.ImAppReleaseEditI
|
|||||||
func (s *sSysImAppRelease) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
func (s *sSysImAppRelease) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||||
return db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields(
|
return db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields(
|
||||||
"id",
|
"id",
|
||||||
|
"app_code",
|
||||||
"platform",
|
"platform",
|
||||||
"channel",
|
"channel",
|
||||||
"latest_version",
|
"latest_version",
|
||||||
@@ -125,29 +130,43 @@ func (s *sSysImAppRelease) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImAppRelease) ensureDefaults(ctx context.Context, db gdb.DB) (err error) {
|
func (s *sSysImAppRelease) ensureDefaults(ctx context.Context, db gdb.DB) (err error) {
|
||||||
|
apps := []string{consts.ImAppCodeIm, consts.ImAppCodeGarden}
|
||||||
defaults := []g.Map{
|
defaults := []g.Map{
|
||||||
{"platform": consts.ImAppPlatformIos, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
|
{"platform": consts.ImAppPlatformIos, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
|
||||||
{"platform": consts.ImAppPlatformAndroid, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
|
{"platform": consts.ImAppPlatformAndroid, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
|
||||||
}
|
}
|
||||||
now := gtime.Now()
|
now := gtime.Now()
|
||||||
|
for _, appCode := range apps {
|
||||||
for _, item := range defaults {
|
for _, item := range defaults {
|
||||||
var count int
|
var count int
|
||||||
if count, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).
|
if count, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).
|
||||||
|
Where("app_code", appCode).
|
||||||
Where("platform", item["platform"]).Where("channel", item["channel"]).Count(); err != nil {
|
Where("platform", item["platform"]).Where("channel", item["channel"]).Count(); err != nil {
|
||||||
return gerror.Wrap(err, "检查IM客户端默认版本策略失败,请稍后重试!")
|
return gerror.Wrap(err, "检查IM客户端默认版本策略失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item["store_url"] = ""
|
row := g.Map{
|
||||||
item["package_url"] = ""
|
"app_code": appCode,
|
||||||
item["checksum"] = ""
|
"platform": item["platform"],
|
||||||
item["published_at"] = now
|
"channel": item["channel"],
|
||||||
item["created_at"] = now
|
"latest_version": item["latest_version"],
|
||||||
item["updated_at"] = now
|
"min_supported_version": item["min_supported_version"],
|
||||||
if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil {
|
"update_type": item["update_type"],
|
||||||
|
"release_notes": item["release_notes"],
|
||||||
|
"remind_after_seconds": item["remind_after_seconds"],
|
||||||
|
"store_url": "",
|
||||||
|
"package_url": "",
|
||||||
|
"checksum": "",
|
||||||
|
"published_at": now,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}
|
||||||
|
if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(row).Insert(); err != nil {
|
||||||
return gerror.Wrap(err, "初始化IM客户端默认版本策略失败,请稍后重试!")
|
return gerror.Wrap(err, "初始化IM客户端默认版本策略失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (s *sSysImFeedbackConfig) Get(ctx context.Context, in *sysin.ImFeedbackConf
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res = &sysin.ImFeedbackConfigModel{}
|
res = &sysin.ImFeedbackConfigModel{}
|
||||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/feedback-config", nil, res); err != nil {
|
if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/feedback-config", nil, tenant.Id, tenant.Code, res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if res != nil {
|
if res != nil {
|
||||||
@@ -52,13 +52,13 @@ func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackC
|
|||||||
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||||
attachmentPolicy = in.AttachmentPolicy.String()
|
attachmentPolicy = in.AttachmentPolicy.String()
|
||||||
}
|
}
|
||||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/feedback-config", g.Map{
|
if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/feedback-config", g.Map{
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
"max_length": in.MaxLength,
|
"max_length": in.MaxLength,
|
||||||
"attachment_policy": attachmentPolicy,
|
"attachment_policy": attachmentPolicy,
|
||||||
"allow_contact": in.AllowContact,
|
"allow_contact": in.AllowContact,
|
||||||
"diagnostics_notice": in.DiagnosticsNotice,
|
"diagnostics_notice": in.DiagnosticsNotice,
|
||||||
}, nil); err != nil {
|
}, tenant.Id, tenant.Code, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (s *sSysImGroup) List(ctx context.Context, in *sysin.ImGroupListInp) (list
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mod := s.groupModel(ctx, db)
|
mod := s.groupModel(ctx, db, tenant.Id)
|
||||||
mod = s.applyGroupWhere(mod, in)
|
mod = s.applyGroupWhere(mod, in)
|
||||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("t.touchedat").OrderDesc("t.id")
|
mod = mod.Page(in.Page, in.PerPage).OrderDesc("t.touchedat").OrderDesc("t.id")
|
||||||
|
|
||||||
@@ -67,11 +67,11 @@ func (s *sSysImGroup) List(ctx context.Context, in *sysin.ImGroupListInp) (list
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) groupModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
func (s *sSysImGroup) groupModel(ctx context.Context, db gdb.DB, tenantId int64) *gdb.Model {
|
||||||
return db.Model("im_topics t").Safe().Ctx(ctx).
|
return db.Model(tenantTablePrefix(tenantId)+"topics t").Safe().Ctx(ctx).
|
||||||
LeftJoin("im_group_settings gs", "gs.topic = t.name").
|
LeftJoin("im_group_settings gs", "gs.tenant_id = t.tenant_id AND gs.topic = t.name").
|
||||||
LeftJoin("im_users owner", "owner.id = COALESCE(gs.owner_user_id, t.owner)").
|
LeftJoin("im_users owner", "owner.tenant_id = t.tenant_id AND owner.id = COALESCE(gs.owner_user_id, t.owner)").
|
||||||
LeftJoin("im_user_profiles owner_profile", "owner_profile.user_id = COALESCE(gs.owner_user_id, t.owner)").
|
LeftJoin("im_user_profiles owner_profile", "owner_profile.tenant_id = t.tenant_id AND owner_profile.user_id = COALESCE(gs.owner_user_id, t.owner)").
|
||||||
Fields(
|
Fields(
|
||||||
"t.name topic",
|
"t.name topic",
|
||||||
"t.state",
|
"t.state",
|
||||||
@@ -103,6 +103,7 @@ func (s *sSysImGroup) groupModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
|||||||
"COALESCE(gs.version, 0) settings_version",
|
"COALESCE(gs.version, 0) settings_version",
|
||||||
"gs.updated_at settings_updated_at",
|
"gs.updated_at settings_updated_at",
|
||||||
).
|
).
|
||||||
|
Where("t.tenant_id", tenantId).
|
||||||
WhereLike("t.name", "grp%")
|
WhereLike("t.name", "grp%")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +140,10 @@ func (s *sSysImGroup) View(ctx context.Context, in *sysin.ImGroupViewInp) (res *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.Model("im_topics t").Safe().Ctx(ctx).
|
err = db.Model(tenantTablePrefix(tenant.Id)+"topics t").Safe().Ctx(ctx).
|
||||||
LeftJoin("im_group_settings gs", "gs.topic = t.name").
|
LeftJoin("im_group_settings gs", "gs.tenant_id = t.tenant_id AND gs.topic = t.name").
|
||||||
LeftJoin("im_users owner", "owner.id = COALESCE(gs.owner_user_id, t.owner)").
|
LeftJoin("im_users owner", "owner.tenant_id = t.tenant_id AND owner.id = COALESCE(gs.owner_user_id, t.owner)").
|
||||||
LeftJoin("im_user_profiles owner_profile", "owner_profile.user_id = COALESCE(gs.owner_user_id, t.owner)").
|
LeftJoin("im_user_profiles owner_profile", "owner_profile.tenant_id = t.tenant_id AND owner_profile.user_id = COALESCE(gs.owner_user_id, t.owner)").
|
||||||
Fields(
|
Fields(
|
||||||
"t.name topic",
|
"t.name topic",
|
||||||
"t.state",
|
"t.state",
|
||||||
@@ -176,6 +177,7 @@ func (s *sSysImGroup) View(ctx context.Context, in *sysin.ImGroupViewInp) (res *
|
|||||||
"COALESCE(gs.version, 0) settings_version",
|
"COALESCE(gs.version, 0) settings_version",
|
||||||
"gs.updated_at settings_updated_at",
|
"gs.updated_at settings_updated_at",
|
||||||
).
|
).
|
||||||
|
Where("t.tenant_id", tenant.Id).
|
||||||
Where("t.name", in.Topic).
|
Where("t.name", in.Topic).
|
||||||
WhereLike("t.name", "grp%").
|
WhereLike("t.name", "grp%").
|
||||||
Scan(&res)
|
Scan(&res)
|
||||||
@@ -196,27 +198,27 @@ func (s *sSysImGroup) MemberList(ctx context.Context, in *sysin.ImGroupMemberLis
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, _, err := resolveImTenantDB(ctx, in.TenantId)
|
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.hasGroupMemberTruth(ctx, db, in.Topic) {
|
if s.hasGroupMemberTruth(ctx, db, tenant.Id, in.Topic) {
|
||||||
return s.memberListFromBusiness(ctx, db, in)
|
return s.memberListFromBusiness(ctx, db, tenant.Id, in)
|
||||||
}
|
}
|
||||||
return s.memberListFromProjection(ctx, db, in)
|
return s.memberListFromProjection(ctx, db, tenant.Id, in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) hasGroupMemberTruth(ctx context.Context, db gdb.DB, topic string) bool {
|
func (s *sSysImGroup) hasGroupMemberTruth(ctx context.Context, db gdb.DB, tenantId int64, topic string) bool {
|
||||||
count, err := db.Model("im_group_members").Safe().Ctx(ctx).Where("topic", topic).Count()
|
count, err := db.Model(tenantTablePrefix(tenantId)+"group_members").Safe().Ctx(ctx).Where("tenant_id", tenantId).Where("topic", topic).Count()
|
||||||
return err == nil && count > 0
|
return err == nil && count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, tenantId int64, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
||||||
mod := db.Model("im_group_members gm").Safe().Ctx(ctx).
|
mod := db.Model(tenantTablePrefix(tenantId)+"group_members gm").Safe().Ctx(ctx).
|
||||||
LeftJoin("im_users u", "u.id = gm.user_id").
|
LeftJoin("im_users u", "u.tenant_id = gm.tenant_id AND u.id = gm.user_id").
|
||||||
LeftJoin("im_user_profiles up", "up.user_id = gm.user_id").
|
LeftJoin("im_user_profiles up", "up.tenant_id = gm.tenant_id AND up.user_id = gm.user_id").
|
||||||
LeftJoin("im_subscriptions s", "s.topic = gm.topic AND s.userid = gm.user_id").
|
LeftJoin(tenantTablePrefix(tenantId)+"subscriptions s", "s.tenant_id = gm.tenant_id AND s.topic = gm.topic AND s.userid = gm.user_id").
|
||||||
Fields(
|
Fields(
|
||||||
"gm.user_id",
|
"gm.user_id",
|
||||||
"up.nickname",
|
"up.nickname",
|
||||||
@@ -242,6 +244,7 @@ func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in
|
|||||||
"gm.removed_by",
|
"gm.removed_by",
|
||||||
"gm.version",
|
"gm.version",
|
||||||
).
|
).
|
||||||
|
Where("gm.tenant_id", tenantId).
|
||||||
Where("gm.topic", in.Topic)
|
Where("gm.topic", in.Topic)
|
||||||
if in.UserId > 0 {
|
if in.UserId > 0 {
|
||||||
mod = mod.Where("gm.user_id", in.UserId)
|
mod = mod.Where("gm.user_id", in.UserId)
|
||||||
@@ -260,10 +263,10 @@ func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, tenantId int64, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
||||||
mod := db.Model("im_subscriptions s").Safe().Ctx(ctx).
|
mod := db.Model(tenantTablePrefix(tenantId)+"subscriptions s").Safe().Ctx(ctx).
|
||||||
LeftJoin("im_users u", "u.id = s.userid").
|
LeftJoin("im_users u", "u.tenant_id = s.tenant_id AND u.id = s.userid").
|
||||||
LeftJoin("im_user_profiles up", "up.user_id = s.userid").
|
LeftJoin("im_user_profiles up", "up.tenant_id = s.tenant_id AND up.user_id = s.userid").
|
||||||
Fields(
|
Fields(
|
||||||
"s.userid user_id",
|
"s.userid user_id",
|
||||||
"up.nickname",
|
"up.nickname",
|
||||||
@@ -289,6 +292,7 @@ func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, i
|
|||||||
"0 removed_by",
|
"0 removed_by",
|
||||||
"0 version",
|
"0 version",
|
||||||
).
|
).
|
||||||
|
Where("s.tenant_id", tenantId).
|
||||||
Where("s.topic", in.Topic)
|
Where("s.topic", in.Topic)
|
||||||
|
|
||||||
if in.UserId > 0 {
|
if in.UserId > 0 {
|
||||||
@@ -335,6 +339,7 @@ func (s *sSysImGroup) Governance(ctx context.Context, in *sysin.ImGroupTopicInp)
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
).
|
).
|
||||||
|
Where("tenant_id", tenant.Id).
|
||||||
Where("topic", in.Topic).
|
Where("topic", in.Topic).
|
||||||
Scan(&res)
|
Scan(&res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -466,7 +471,7 @@ func (s *sSysImGroup) JoinRequestReject(ctx context.Context, in *sysin.ImGroupJo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) resolveJoinRequest(ctx context.Context, in *sysin.ImGroupJoinRequestResolveInp, status string) (err error) {
|
func (s *sSysImGroup) resolveJoinRequest(ctx context.Context, in *sysin.ImGroupJoinRequestResolveInp, status string) (err error) {
|
||||||
db, _, err := resolveImTenantDB(ctx, in.TenantId)
|
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -492,10 +497,10 @@ func (s *sSysImGroup) resolveJoinRequest(ctx context.Context, in *sysin.ImGroupJ
|
|||||||
}
|
}
|
||||||
applicantUserId := before["applicant_user_id"].Int64()
|
applicantUserId := before["applicant_user_id"].Int64()
|
||||||
if status == consts.ImGroupRequestStatusAccepted {
|
if status == consts.ImGroupRequestStatusAccepted {
|
||||||
if txErr = s.upsertGroupSubscription(ctx, tx, in.Topic, applicantUserId, imGroupModeMember, now); txErr != nil {
|
if txErr = s.upsertGroupSubscription(ctx, tx, tenant.Id, in.Topic, applicantUserId, imGroupModeMember, now); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
if txErr = s.upsertGroupMember(ctx, tx, in.Topic, applicantUserId, consts.ImGroupRoleMember, "approval", actor, now); txErr != nil {
|
if txErr = s.upsertGroupMember(ctx, tx, tenant.Id, in.Topic, applicantUserId, consts.ImGroupRoleMember, "approval", actor, now); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,7 +514,7 @@ func (s *sSysImGroup) resolveJoinRequest(ctx context.Context, in *sysin.ImGroupJ
|
|||||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||||
return gerror.New("入群申请已处理")
|
return gerror.New("入群申请已处理")
|
||||||
}
|
}
|
||||||
if txErr = s.recountGroupMembers(ctx, tx, in.Topic); txErr != nil {
|
if txErr = s.recountGroupMembers(ctx, tx, tenant.Id, in.Topic); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
after, txErr := s.groupJoinRequestSnapshot(ctx, tx, in.Topic, in.RequestId)
|
after, txErr := s.groupJoinRequestSnapshot(ctx, tx, in.Topic, in.RequestId)
|
||||||
@@ -639,8 +644,8 @@ func (s *sSysImGroup) groupJoinRequestSnapshot(ctx context.Context, tx gdb.TX, t
|
|||||||
One()
|
One()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) groupMemberSnapshot(ctx context.Context, tx gdb.TX, topic string, userId int64) (res gdb.Record, err error) {
|
func (s *sSysImGroup) groupMemberSnapshot(ctx context.Context, tx gdb.TX, tenantId int64, topic string, userId int64) (res gdb.Record, err error) {
|
||||||
return tx.Model("im_group_members").Safe().Ctx(ctx).
|
return tx.Model(tenantTablePrefix(tenantId)+"group_members").Safe().Ctx(ctx).
|
||||||
Where("topic", topic).
|
Where("topic", topic).
|
||||||
Where("user_id", userId).
|
Where("user_id", userId).
|
||||||
One()
|
One()
|
||||||
@@ -678,8 +683,8 @@ func (s *sSysImGroup) groupMuteSnapshot(ctx context.Context, tx gdb.TX, topic st
|
|||||||
One()
|
One()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) ensureActiveGroupMember(ctx context.Context, tx gdb.TX, topic string, userId int64) error {
|
func (s *sSysImGroup) ensureActiveGroupMember(ctx context.Context, tx gdb.TX, tenantId int64, topic string, userId int64) error {
|
||||||
count, err := tx.Model("im_group_members").Safe().Ctx(ctx).
|
count, err := tx.Model(tenantTablePrefix(tenantId)+"group_members").Safe().Ctx(ctx).
|
||||||
Where("topic", topic).
|
Where("topic", topic).
|
||||||
Where("user_id", userId).
|
Where("user_id", userId).
|
||||||
Where("member_status", consts.ImGroupMemberStatusActive).
|
Where("member_status", consts.ImGroupMemberStatusActive).
|
||||||
@@ -693,8 +698,8 @@ func (s *sSysImGroup) ensureActiveGroupMember(ctx context.Context, tx gdb.TX, to
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) ensureAdminLimit(ctx context.Context, tx gdb.TX, topic string, userId int64) error {
|
func (s *sSysImGroup) ensureAdminLimit(ctx context.Context, tx gdb.TX, tenantId int64, topic string, userId int64) error {
|
||||||
currentRole, err := tx.Model("im_group_members").Safe().Ctx(ctx).
|
currentRole, err := tx.Model(tenantTablePrefix(tenantId)+"group_members").Safe().Ctx(ctx).
|
||||||
Where("topic", topic).
|
Where("topic", topic).
|
||||||
Where("user_id", userId).
|
Where("user_id", userId).
|
||||||
Where("member_status", consts.ImGroupMemberStatusActive).
|
Where("member_status", consts.ImGroupMemberStatusActive).
|
||||||
@@ -712,7 +717,7 @@ func (s *sSysImGroup) ensureAdminLimit(ctx context.Context, tx gdb.TX, topic str
|
|||||||
if maxAdminCount.Int() <= 0 {
|
if maxAdminCount.Int() <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
adminCount, err := tx.Model("im_group_members").Safe().Ctx(ctx).
|
adminCount, err := tx.Model(tenantTablePrefix(tenantId)+"group_members").Safe().Ctx(ctx).
|
||||||
Where("topic", topic).
|
Where("topic", topic).
|
||||||
Where("role", consts.ImGroupRoleAdmin).
|
Where("role", consts.ImGroupRoleAdmin).
|
||||||
Where("member_status", consts.ImGroupMemberStatusActive).
|
Where("member_status", consts.ImGroupMemberStatusActive).
|
||||||
@@ -726,33 +731,38 @@ func (s *sSysImGroup) ensureAdminLimit(ctx context.Context, tx gdb.TX, topic str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) upsertGroupSubscription(ctx context.Context, tx gdb.TX, topic string, userId int64, modeGiven string, now *gtime.Time) error {
|
func (s *sSysImGroup) upsertGroupSubscription(ctx context.Context, tx gdb.TX, tenantId int64, topic string, userId int64, modeGiven string, now *gtime.Time) error {
|
||||||
_, err := tx.Exec(`INSERT INTO im_subscriptions(createdat,updatedat,deletedat,userid,topic,modewant,modegiven,private)
|
table := tenantTablePrefix(tenantId) + "subscriptions"
|
||||||
VALUES(?,?,NULL,?,?,?,?,CAST('{}' AS JSON))
|
_, err := tx.Exec(fmt.Sprintf(`INSERT INTO %s(tenant_id,createdat,updatedat,deletedat,userid,topic,modewant,modegiven,private)
|
||||||
|
VALUES(?,?,?,NULL,?,?,?,?,CAST('{}' AS JSON))
|
||||||
ON DUPLICATE KEY UPDATE createdat=VALUES(createdat),updatedat=VALUES(updatedat),deletedat=NULL,
|
ON DUPLICATE KEY UPDATE createdat=VALUES(createdat),updatedat=VALUES(updatedat),deletedat=NULL,
|
||||||
modewant=VALUES(modewant),modegiven=VALUES(modegiven),delid=0,recvseqid=0,readseqid=0`,
|
modewant=VALUES(modewant),modegiven=VALUES(modegiven),delid=0,recvseqid=0,readseqid=0`, table),
|
||||||
now, now, userId, topic, imGroupModeMember, modeGiven)
|
tenantId, now, now, userId, topic, imGroupModeMember, modeGiven)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) upsertGroupMember(ctx context.Context, tx gdb.TX, topic string, userId int64, role string, joinSource string, joinedBy int64, now *gtime.Time) error {
|
func (s *sSysImGroup) upsertGroupMember(ctx context.Context, tx gdb.TX, tenantId int64, topic string, userId int64, role string, joinSource string, joinedBy int64, now *gtime.Time) error {
|
||||||
_, err := tx.Exec(`INSERT INTO im_group_members(topic,user_id,role,member_status,join_source,joined_by,joined_at,version,created_at,updated_at)
|
table := tenantTablePrefix(tenantId) + "group_members"
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?)
|
_, err := tx.Exec(fmt.Sprintf(`INSERT INTO %s(tenant_id,topic,user_id,role,member_status,join_source,joined_by,joined_at,version,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),join_source=VALUES(join_source),
|
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),join_source=VALUES(join_source),
|
||||||
joined_by=VALUES(joined_by),joined_at=VALUES(joined_at),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`,
|
joined_by=VALUES(joined_by),joined_at=VALUES(joined_at),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`, table),
|
||||||
topic, userId, role, consts.ImGroupMemberStatusActive, joinSource, joinedBy, now, 1, now, now)
|
tenantId, topic, userId, role, consts.ImGroupMemberStatusActive, joinSource, joinedBy, now, 1, now, now)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImGroup) recountGroupMembers(ctx context.Context, tx gdb.TX, topic string) error {
|
func (s *sSysImGroup) recountGroupMembers(ctx context.Context, tx gdb.TX, tenantId int64, topic string) error {
|
||||||
if _, err := tx.Exec(`UPDATE im_group_settings
|
membersTable := tenantTablePrefix(tenantId) + "group_members"
|
||||||
SET member_count=(SELECT COUNT(*) FROM im_group_members WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3)
|
subscriptionsTable := tenantTablePrefix(tenantId) + "subscriptions"
|
||||||
WHERE topic=?`, topic, consts.ImGroupMemberStatusActive, topic); err != nil {
|
topicsTable := tenantTablePrefix(tenantId) + "topics"
|
||||||
|
if _, err := tx.Exec(fmt.Sprintf(`UPDATE im_group_settings
|
||||||
|
SET member_count=(SELECT COUNT(*) FROM %s WHERE tenant_id=? AND topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3)
|
||||||
|
WHERE tenant_id=? AND topic=?`, membersTable), tenantId, topic, consts.ImGroupMemberStatusActive, tenantId, topic); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := tx.Exec(`UPDATE im_topics
|
_, err := tx.Exec(fmt.Sprintf(`UPDATE %s
|
||||||
SET subcnt=(SELECT COUNT(*) FROM im_subscriptions WHERE topic=? AND deletedat IS NULL),updatedat=CURRENT_TIMESTAMP(3)
|
SET subcnt=(SELECT COUNT(*) FROM %s WHERE tenant_id=? AND topic=? AND deletedat IS NULL),updatedat=CURRENT_TIMESTAMP(3)
|
||||||
WHERE name=?`, topic, topic)
|
WHERE tenant_id=? AND name=?`, topicsTable, subscriptionsTable), tenantId, topic, tenantId, topic)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (s *sSysImMailboxConfig) Get(ctx context.Context, in *sysin.ImMailboxConfig
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res = &sysin.ImMailboxConfigModel{}
|
res = &sysin.ImMailboxConfigModel{}
|
||||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/mailbox-config", nil, res); err != nil {
|
if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/mailbox-config", nil, tenant.Id, tenant.Code, res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if res != nil {
|
if res != nil {
|
||||||
@@ -52,7 +52,7 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
|||||||
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||||
attachmentPolicy = in.AttachmentPolicy.String()
|
attachmentPolicy = in.AttachmentPolicy.String()
|
||||||
}
|
}
|
||||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/mailbox-config", g.Map{
|
if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/mailbox-config", g.Map{
|
||||||
"enabled": in.Enabled,
|
"enabled": in.Enabled,
|
||||||
"allow_anonymous": in.AllowAnonymous,
|
"allow_anonymous": in.AllowAnonymous,
|
||||||
"required_fields": requiredFields,
|
"required_fields": requiredFields,
|
||||||
@@ -60,7 +60,7 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
|||||||
"attachment_policy": attachmentPolicy,
|
"attachment_policy": attachmentPolicy,
|
||||||
"notice_title": in.NoticeTitle,
|
"notice_title": in.NoticeTitle,
|
||||||
"notice_content": in.NoticeContent,
|
"notice_content": in.NoticeContent,
|
||||||
}, nil); err != nil {
|
}, tenant.Id, tenant.Code, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
|
||||||
|
|||||||
@@ -40,10 +40,9 @@ func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageE
|
|||||||
}
|
}
|
||||||
db := imtenant.RegistryDB()
|
db := imtenant.RegistryDB()
|
||||||
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
|
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
|
||||||
LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id").
|
|
||||||
Fields(
|
Fields(
|
||||||
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
|
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
|
||||||
"COALESCE(c."+imTenantMessageEncryptionColumn+",0) message_encryption_enabled",
|
"t."+imTenantMessageEncryptionColumn+" message_encryption_enabled",
|
||||||
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version",
|
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version",
|
||||||
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version",
|
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version",
|
||||||
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
|
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
|
||||||
@@ -56,7 +55,7 @@ func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageE
|
|||||||
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||||
}
|
}
|
||||||
if in.Enabled != nil {
|
if in.Enabled != nil {
|
||||||
mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled)
|
mod = mod.Where("t."+imTenantMessageEncryptionColumn, *in.Enabled)
|
||||||
}
|
}
|
||||||
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
|
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
|
||||||
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
|
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
|
||||||
@@ -185,16 +184,16 @@ func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessag
|
|||||||
return gerror.New("启用消息加密前必须先初始化有效密钥")
|
return gerror.New("启用消息加密前必须先初始化有效密钥")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
result, err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).
|
Where(dao.ImTenant.Columns().Id, tenant.Id).
|
||||||
Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
|
Data(g.Map{dao.ImTenant.Columns().MessageEncryptionEnabled: in.Enabled, dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "更新商户消息加密开关失败")
|
return gerror.Wrap(err, "更新商户消息加密开关失败")
|
||||||
}
|
}
|
||||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||||
return gerror.New("商户配置不存在")
|
return gerror.New("商户配置不存在")
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled})
|
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", dao.ImTenant.Table(), tenant.Id, "success", g.Map{"enabled": in.Enabled})
|
||||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
|
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +202,8 @@ func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessag
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||||
Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value()
|
Fields(dao.ImTenant.Columns().MessageEncryptionEnabled).Where(dao.ImTenant.Columns().Id, tenant.Id).Value()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
|
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import (
|
|||||||
"hotgo/internal/library/imkafka"
|
"hotgo/internal/library/imkafka"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
)
|
)
|
||||||
@@ -43,6 +45,38 @@ func policyJSON(value *gjson.Json, fallback string) string {
|
|||||||
return value.String()
|
return value.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imPolicyTemplateTable 安全策略模板表(平台级单行,新建商户初始策略来源)
|
||||||
|
const imPolicyTemplateTable = "im_security_policy_template"
|
||||||
|
|
||||||
|
// loadPolicyTemplateRow 读取安全策略模板单行(取最小 id),返回 nil 表示模板未初始化。
|
||||||
|
func loadPolicyTemplateRow(model *gdb.Model) (gdb.Record, error) {
|
||||||
|
row, err := model.Order("id ASC").Limit(1).One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if row.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateCurfewRules 解析模板行 curfew_rules JSON 为宵禁规则输入列表
|
||||||
|
func templateCurfewRules(tpl gdb.Record) []*sysin.ImMessageCurfewRuleInp {
|
||||||
|
rules := make([]*sysin.ImMessageCurfewRuleInp, 0)
|
||||||
|
for _, item := range gjson.New(tpl["curfew_rules"].String()).Array() {
|
||||||
|
j := gjson.New(item)
|
||||||
|
rules = append(rules, &sysin.ImMessageCurfewRuleInp{
|
||||||
|
RuleCode: j.Get("ruleCode").String(), RuleName: j.Get("ruleName").String(), DateType: j.Get("dateType").String(),
|
||||||
|
CustomDates: gjson.New(j.Get("customDates").String()), Weekdays: gjson.New(j.Get("weekdays").String()),
|
||||||
|
Timezone: j.Get("timezone").String(), StartTime: j.Get("startTime").String(), EndTime: j.Get("endTime").String(),
|
||||||
|
Scope: j.Get("scope").String(),
|
||||||
|
WhitelistUserIds: gjson.New(j.Get("whitelistUserIds").String()), WhitelistGroupIds: gjson.New(j.Get("whitelistGroupIds").String()),
|
||||||
|
Message: j.Get("message").String(), Status: j.Get("status").Int(), Sort: j.Get("sort").Int(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
func publishImPolicyEvent(ctx context.Context, tenantID int64, tenantCode, eventType, group string, version uint) error {
|
func publishImPolicyEvent(ctx context.Context, tenantID int64, tenantCode, eventType, group string, version uint) error {
|
||||||
return imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
return imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Package sys
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/library/storager"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadImPublicUploads(ctx context.Context, fileIds []string) (files []*sysin.ImPublicUploadModel, err error) {
|
||||||
|
fileIds = compactStringSlice(fileIds)
|
||||||
|
if len(fileIds) == 0 {
|
||||||
|
return []*sysin.ImPublicUploadModel{}, nil
|
||||||
|
}
|
||||||
|
placeholders := make([]string, 0, len(fileIds))
|
||||||
|
args := make([]any, 0, len(fileIds))
|
||||||
|
for _, fileId := range fileIds {
|
||||||
|
placeholders = append(placeholders, "?")
|
||||||
|
args = append(args, fileId)
|
||||||
|
}
|
||||||
|
query := `SELECT id,file_id,purpose,mime_type,size,etag,location,expires_at,created_at,uploaded_at
|
||||||
|
FROM im_public_uploads WHERE file_id IN (` + strings.Join(placeholders, ",") + `) ORDER BY id ASC`
|
||||||
|
rows, err := g.DB().GetAll(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取公共上传文件信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if err = rows.Structs(&files); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "解析公共上传文件信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
|
||||||
|
drive := ""
|
||||||
|
if conf, confErr := service.SysConfig().GetUpload(ctx); confErr == nil && conf != nil {
|
||||||
|
drive = conf.Drive
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
file.Url = storager.LastUrl(ctx, file.Location, drive)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstImPublicUpload(ctx context.Context, fileId string) (file *sysin.ImPublicUploadModel, err error) {
|
||||||
|
files, err := loadImPublicUploads(ctx, []string{fileId})
|
||||||
|
if err != nil || len(files) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return files[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJSONStringSlice(raw string) []string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
if err := gjson.DecodeTo([]byte(raw), &ids); err == nil && len(ids) > 0 {
|
||||||
|
return compactStringSlice(ids)
|
||||||
|
}
|
||||||
|
return compactStringSlice([]string{raw})
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactStringSlice(values []string) []string {
|
||||||
|
res := make([]string, 0, len(values))
|
||||||
|
seen := make(map[string]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
res = append(res, value)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -130,7 +130,7 @@ func (s *sSysImRegistration) seedConfig(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImRegistration) verifySuper(ctx context.Context) error {
|
func (s *sSysImRegistration) verifySuper(ctx context.Context) error {
|
||||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||||
return gerror.New("仅平台超级管理员可操作IM注册配置")
|
return gerror.New("仅平台超级管理员可操作IM注册配置")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -65,8 +65,34 @@ func (s *sSysImSecurityPolicy) View(ctx context.Context, in *sysin.ImSecurityPol
|
|||||||
err = baseModel.Where(c.TenantId, tenantID).
|
err = baseModel.Where(c.TenantId, tenantID).
|
||||||
Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||||
OrderDesc(c.PolicyVersion).Scan(&res)
|
OrderDesc(c.PolicyVersion).Scan(&res)
|
||||||
if err == nil && res == nil {
|
if err != nil {
|
||||||
err = baseModel.Where(c.TenantId, tenantID).OrderDesc(c.PolicyVersion).Scan(&res)
|
return nil, gerror.Wrap(err, "获取安全策略失败")
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
// 无自建策略时回落默认模板展示,保存后成为本商户自己的策略
|
||||||
|
tpl, tplErr := loadPolicyTemplateRow(imtenant.RegistryDB().Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||||
|
if tplErr != nil {
|
||||||
|
return nil, gerror.Wrap(tplErr, "读取安全策略模板失败")
|
||||||
|
}
|
||||||
|
if tpl != nil {
|
||||||
|
res = &sysin.ImSecurityPolicyModel{
|
||||||
|
TenantId: tenantID,
|
||||||
|
PolicyName: tpl["policy_name"].String(),
|
||||||
|
PasswordPolicy: tpl["password_policy"].String(),
|
||||||
|
LoginRateLimit: tpl["login_rate_limit"].String(),
|
||||||
|
SensitiveWordPolicy: tpl["sensitive_word_policy"].String(),
|
||||||
|
FileReviewPolicy: tpl["file_review_policy"].String(),
|
||||||
|
RealnamePolicy: tpl["realname_policy"].String(),
|
||||||
|
RiskChallengePolicy: tpl["risk_challenge_policy"].String(),
|
||||||
|
GlobalFriendEnabled: tpl["global_friend_enabled"].Int(),
|
||||||
|
FriendDisabledPolicy: tpl["friend_disabled_policy"].String(),
|
||||||
|
MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(),
|
||||||
|
}
|
||||||
|
for _, rule := range templateCurfewRules(tpl) {
|
||||||
|
res.CurfewRules = append(res.CurfewRules, &sysin.ImMessageCurfewRuleModel{RuleCode: rule.RuleCode, RuleName: rule.RuleName, DateType: rule.DateType, CustomDates: rule.CustomDates.String(), Weekdays: rule.Weekdays.String(), Timezone: rule.Timezone, StartTime: rule.StartTime, EndTime: rule.EndTime, Scope: rule.Scope, WhitelistUserIds: rule.WhitelistUserIds.String(), WhitelistGroupIds: rule.WhitelistGroupIds.String(), Message: rule.Message, Status: rule.Status, Sort: rule.Sort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,6 +114,8 @@ func (s *sSysImSecurityPolicy) curfewRules(ctx context.Context, policyID int64)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save 保存安全策略并立即生效:每租户仅一条当前生效记录(单记录模型,无草稿/发布工作流)。
|
||||||
|
// 已有记录则原地更新(版本号自增仅作审计),无则创建;同步 im_tenant 冗余开关并投递策略变更事件。
|
||||||
func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error) {
|
func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
@@ -96,55 +124,52 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if tenantID == 0 {
|
||||||
|
// 默认模板不可通过商户策略保存入口修改
|
||||||
|
return 0, gerror.New("默认模板不可修改,请选择具体商户")
|
||||||
|
}
|
||||||
db := imtenant.RegistryDB()
|
db := imtenant.RegistryDB()
|
||||||
c := dao.ImSecurityPolicy.Columns()
|
c := dao.ImSecurityPolicy.Columns()
|
||||||
rc := dao.ImMessageCurfewRules.Columns()
|
rc := dao.ImMessageCurfewRules.Columns()
|
||||||
|
tc := dao.ImTenant.Columns()
|
||||||
operator := contexts.GetUserId(ctx)
|
operator := contexts.GetUserId(ctx)
|
||||||
now := gtime.Now()
|
now := gtime.Now()
|
||||||
|
version := uint(1)
|
||||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||||
version := uint(0)
|
current, readErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||||
if in.Id > 0 {
|
Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||||
current, readErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One()
|
WhereNull(c.DeletedAt).OrderDesc(c.PolicyVersion).One()
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
return readErr
|
return readErr
|
||||||
}
|
}
|
||||||
if current.IsEmpty() || current[c.TenantId].Int64() != tenantID {
|
data := g.Map{c.PolicyName: in.PolicyName, c.PublishStatus: consts.ImSecurityPublishPublished, c.PasswordPolicy: policyJSON(in.PasswordPolicy, "{}"), c.LoginRateLimit: policyJSON(in.LoginRateLimit, "{}"), c.SensitiveWordPolicy: normalizedModerationPolicy(in.SensitiveWordPolicy, tenantCode), c.FileReviewPolicy: policyJSON(in.FileReviewPolicy, "{}"), c.RealnamePolicy: policyJSON(in.RealnamePolicy, "{}"), c.RiskChallengePolicy: policyJSON(in.RiskChallengePolicy, "{}"), c.GlobalFriendEnabled: in.GlobalFriendEnabled, c.FriendDisabledPolicy: policyJSON(in.FriendDisabledPolicy, `{"hide_profile":true,"block_qrcode":true,"block_group_member_entry":true,"blocked_entries":["search","profile_card","group_member","qrcode","invite_auto_add","recommendation"],"message":"当前时段暂不允许添加好友"}`), c.MessageCurfewEnabled: in.MessageCurfewEnabled, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator}
|
||||||
return gerror.New("安全策略不存在")
|
if in.LoginRateLimit == nil {
|
||||||
}
|
// 未提交登录失败策略时保留原值(登录失败锁定由独立配置入口管理,避免保存时清空配置)
|
||||||
if current[c.PublishStatus].String() != consts.ImSecurityPublishDraft {
|
|
||||||
return gerror.New("只有草稿状态的安全策略可以编辑")
|
|
||||||
}
|
|
||||||
version = current[c.PolicyVersion].Uint()
|
|
||||||
id = in.Id
|
|
||||||
} else {
|
|
||||||
maxValue, maxErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Max(c.PolicyVersion)
|
|
||||||
if maxErr != nil {
|
|
||||||
return maxErr
|
|
||||||
}
|
|
||||||
version = uint(maxValue) + 1
|
|
||||||
}
|
|
||||||
data := g.Map{c.TenantId: tenantID, c.PolicyVersion: version, c.PolicyName: in.PolicyName, c.PublishStatus: consts.ImSecurityPublishDraft, c.PasswordPolicy: policyJSON(in.PasswordPolicy, "{}"), c.LoginRateLimit: policyJSON(in.LoginRateLimit, "{}"), c.SensitiveWordPolicy: normalizedModerationPolicy(in.SensitiveWordPolicy, tenantCode), c.FileReviewPolicy: policyJSON(in.FileReviewPolicy, "{}"), c.RealnamePolicy: policyJSON(in.RealnamePolicy, "{}"), c.RiskChallengePolicy: policyJSON(in.RiskChallengePolicy, "{}"), c.GlobalFriendEnabled: in.GlobalFriendEnabled, c.FriendDisabledPolicy: policyJSON(in.FriendDisabledPolicy, `{"hide_profile":true,"block_qrcode":true,"block_group_member_entry":true,"blocked_entries":["search","profile_card","group_member","qrcode","invite_auto_add","recommendation"],"message":"当前时段暂不允许添加好友"}`), c.MessageCurfewEnabled: in.MessageCurfewEnabled, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator}
|
|
||||||
if in.Id > 0 && in.LoginRateLimit == nil {
|
|
||||||
// 未提交登录失败策略时保留原值(登录失败锁定已迁移至独立配置入口,避免保存草稿清空配置)
|
|
||||||
delete(data, c.LoginRateLimit)
|
delete(data, c.LoginRateLimit)
|
||||||
}
|
}
|
||||||
if in.Id > 0 {
|
if current.IsEmpty() {
|
||||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil {
|
version = 1
|
||||||
return updateErr
|
data[c.TenantId] = tenantID
|
||||||
}
|
data[c.PolicyVersion] = version
|
||||||
} else {
|
|
||||||
data[c.CreatedAt] = now
|
data[c.CreatedAt] = now
|
||||||
data[c.CreatedBy] = operator
|
data[c.CreatedBy] = operator
|
||||||
result, insertErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert()
|
result, insertErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert()
|
||||||
if insertErr != nil {
|
if insertErr != nil {
|
||||||
return insertErr
|
return insertErr
|
||||||
}
|
}
|
||||||
id, insertErr = result.LastInsertId()
|
if id, insertErr = result.LastInsertId(); insertErr != nil {
|
||||||
if insertErr != nil {
|
|
||||||
return insertErr
|
return insertErr
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
id = current[c.Id].Int64()
|
||||||
|
version = current[c.PolicyVersion].Uint() + 1
|
||||||
|
data[c.PolicyVersion] = version
|
||||||
|
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, id).Data(data).Update(); updateErr != nil {
|
||||||
|
return updateErr
|
||||||
}
|
}
|
||||||
if _, deleteErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Where(rc.PolicyId, id).Delete(); deleteErr != nil {
|
}
|
||||||
|
// 规则整体重建:uk_im_message_curfew_rule 唯一键含 tenant_id+rule_code,软删行仍占键会冲突,必须硬删
|
||||||
|
if _, deleteErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Where(rc.PolicyId, id).Unscoped().Delete(); deleteErr != nil {
|
||||||
return deleteErr
|
return deleteErr
|
||||||
}
|
}
|
||||||
for _, rule := range in.CurfewRules {
|
for _, rule := range in.CurfewRules {
|
||||||
@@ -153,12 +178,26 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
|||||||
return insertErr
|
return insertErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if tenantID > 0 {
|
||||||
|
// 上游已将商户配置并入 im_tenant 表,此处同步冗余开关与策略版本。
|
||||||
|
// updated_at 显式自赋值:抑制 gf 自动填充与 ON UPDATE CURRENT_TIMESTAMP,策略保存不 touch 商户行的更新人/时间
|
||||||
|
if _, cfgErr := tx.Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Where(tc.Id, tenantID).
|
||||||
|
Data(g.Map{tc.SecurityPolicyVersion: version, tc.GlobalFriendEnabled: in.GlobalFriendEnabled, tc.MessageCurfewEnabled: in.MessageCurfewEnabled, tc.UpdatedAt: gdb.Raw("updated_at")}).Update(); cfgErr != nil {
|
||||||
|
return cfgErr
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, gerror.Wrap(err, "保存安全策略草稿失败")
|
return 0, gerror.Wrap(err, "保存安全策略失败")
|
||||||
|
}
|
||||||
|
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_save", "im_security_policy", id, "success", g.Map{"version": version, "global_friend_enabled": in.GlobalFriendEnabled, "message_curfew_enabled": in.MessageCurfewEnabled})
|
||||||
|
// 保存即生效:投递策略变更事件,IM 端消费后失效策略缓存并回源拉取新配置。
|
||||||
|
// 事件已持久化到 im_admin_outbox,立即投递失败仅告警+审计、由 outbox 重试续投,不阻塞已生效的保存结果
|
||||||
|
if pubErr := publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version); pubErr != nil {
|
||||||
|
g.Log().Warningf(ctx, "IM策略事件投递失败(outbox将重试): tenant=%d version=%d err=%v", tenantID, version, pubErr)
|
||||||
|
_ = writeImAdminAudit(ctx, tenantID, "im_policy_event_publish", "im_security_policy", id, "failed", g.Map{"error": pubErr.Error(), "version": version})
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_save", "im_security_policy", id, "success", g.Map{"global_friend_enabled": in.GlobalFriendEnabled, "message_curfew_enabled": in.MessageCurfewEnabled})
|
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,51 +215,6 @@ func normalizedModerationPolicy(value *gjson.Json, _ string) string {
|
|||||||
return gjson.New(policy).String()
|
return gjson.New(policy).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImSecurityPolicy) Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) error {
|
|
||||||
if err := in.Filter(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
db := imtenant.RegistryDB()
|
|
||||||
c := dao.ImSecurityPolicy.Columns()
|
|
||||||
var row gdb.Record
|
|
||||||
var err error
|
|
||||||
if row, err = db.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if row.IsEmpty() {
|
|
||||||
return gerror.New("安全策略不存在")
|
|
||||||
}
|
|
||||||
if row[c.PublishStatus].String() != consts.ImSecurityPublishDraft {
|
|
||||||
return gerror.New("只有草稿状态可以发布")
|
|
||||||
}
|
|
||||||
tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
operator := contexts.GetUserId(ctx)
|
|
||||||
now := gtime.Now()
|
|
||||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
||||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNot(c.Id, in.Id).WhereNull(c.DeletedAt).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishArchived, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil {
|
|
||||||
return updateErr
|
|
||||||
}
|
|
||||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishPublished, c.PublishedAt: now, c.PublishedBy: operator, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil {
|
|
||||||
return updateErr
|
|
||||||
}
|
|
||||||
if tenantID > 0 {
|
|
||||||
tc := dao.ImTenantConfig.Columns()
|
|
||||||
if _, updateErr := tx.Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).Where(tc.TenantId, tenantID).Data(g.Map{tc.SecurityPolicyVersion: row[c.PolicyVersion].Uint(), tc.GlobalFriendEnabled: row[c.GlobalFriendEnabled].Int(), tc.MessageCurfewEnabled: row[c.MessageCurfewEnabled].Int(), tc.UpdatedAt: now, tc.UpdatedBy: operator}).Update(); updateErr != nil {
|
|
||||||
return updateErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return gerror.Wrap(err, "发布安全策略失败")
|
|
||||||
}
|
|
||||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_publish", "im_security_policy", in.Id, "success", g.Map{"reason": in.Reason, "version": row[c.PolicyVersion].Uint()})
|
|
||||||
return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", row[c.PolicyVersion].Uint())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (int64, error) {
|
func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (int64, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -229,27 +223,15 @@ func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
c := dao.ImSecurityPolicy.Columns()
|
// 读取安全策略模板,将模板内容覆盖为当前商户的策略(复制后商户可自行修改)
|
||||||
var source *sysin.ImSecurityPolicyModel
|
tpl, err := loadPolicyTemplateRow(imtenant.RegistryDB().Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||||
err = imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
|
||||||
Fields(securityPolicyFields("")...).Where(c.TenantId, 0).
|
|
||||||
Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNull(c.DeletedAt).
|
|
||||||
OrderDesc(c.PolicyVersion).Scan(&source)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, gerror.Wrap(err, "读取全局默认安全策略失败")
|
return 0, gerror.Wrap(err, "读取安全策略模板失败")
|
||||||
}
|
}
|
||||||
if source == nil {
|
if tpl == nil {
|
||||||
return 0, gerror.New("全局默认安全策略不存在")
|
return 0, gerror.New("安全策略模板未初始化")
|
||||||
}
|
}
|
||||||
source.CurfewRules, err = s.curfewRules(ctx, source.Id)
|
id, err := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: tpl["policy_name"].String(), PasswordPolicy: gjson.New(tpl["password_policy"].String()), LoginRateLimit: gjson.New(tpl["login_rate_limit"].String()), SensitiveWordPolicy: gjson.New(tpl["sensitive_word_policy"].String()), FileReviewPolicy: gjson.New(tpl["file_review_policy"].String()), RealnamePolicy: gjson.New(tpl["realname_policy"].String()), RiskChallengePolicy: gjson.New(tpl["risk_challenge_policy"].String()), GlobalFriendEnabled: tpl["global_friend_enabled"].Int(), FriendDisabledPolicy: gjson.New(tpl["friend_disabled_policy"].String()), MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(), Remark: in.Reason, CurfewRules: templateCurfewRules(tpl)})
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
rules := make([]*sysin.ImMessageCurfewRuleInp, 0, len(source.CurfewRules))
|
|
||||||
for _, rule := range source.CurfewRules {
|
|
||||||
rules = append(rules, &sysin.ImMessageCurfewRuleInp{RuleCode: rule.RuleCode, RuleName: rule.RuleName, DateType: rule.DateType, CustomDates: gjson.New(rule.CustomDates), Weekdays: gjson.New(rule.Weekdays), Timezone: rule.Timezone, StartTime: rule.StartTime, EndTime: rule.EndTime, Scope: rule.Scope, WhitelistUserIds: gjson.New(rule.WhitelistUserIds), WhitelistGroupIds: gjson.New(rule.WhitelistGroupIds), Message: rule.Message, Status: rule.Status, Sort: rule.Sort})
|
|
||||||
}
|
|
||||||
id, err := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "恢复默认安全策略", PasswordPolicy: gjson.New(source.PasswordPolicy), LoginRateLimit: gjson.New(source.LoginRateLimit), SensitiveWordPolicy: gjson.New(source.SensitiveWordPolicy), FileReviewPolicy: gjson.New(source.FileReviewPolicy), RealnamePolicy: gjson.New(source.RealnamePolicy), RiskChallengePolicy: gjson.New(source.RiskChallengePolicy), GlobalFriendEnabled: source.GlobalFriendEnabled, FriendDisabledPolicy: gjson.New(source.FriendDisabledPolicy), MessageCurfewEnabled: source.MessageCurfewEnabled, Remark: in.Reason, CurfewRules: rules})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_restore_default", "im_security_policy", id, "success", g.Map{"reason": in.Reason})
|
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_restore_default", "im_security_policy", id, "success", g.Map{"reason": in.Reason})
|
||||||
}
|
}
|
||||||
@@ -326,16 +308,34 @@ func (s *sSysImSecurityPolicy) SaveLoginLockoutConfig(ctx context.Context, in *s
|
|||||||
return gerror.Wrap(err, "保存登录失败策略配置失败")
|
return gerror.Wrap(err, "保存登录失败策略配置失败")
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", policyID, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", policyID, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
||||||
return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version)
|
if pubErr := publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version); pubErr != nil {
|
||||||
|
g.Log().Warningf(ctx, "IM策略事件投递失败(outbox将重试): tenant=%d version=%d err=%v", tenantID, version, pubErr)
|
||||||
|
_ = writeImAdminAudit(ctx, tenantID, "im_policy_event_publish", "im_security_policy", policyID, "failed", g.Map{"error": pubErr.Error(), "version": version})
|
||||||
}
|
}
|
||||||
// 租户尚无已发布策略:创建默认策略草稿并直接发布(Publish 内部已发送策略变更事件)
|
return nil
|
||||||
id, saveErr := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "默认安全策略", LoginRateLimit: gjson.New(value), GlobalFriendEnabled: 1, MessageCurfewEnabled: 0})
|
}
|
||||||
|
// 租户尚无策略:按模板复制初始策略(模板未初始化时退回内置默认值),仅覆盖登录失败配置,保存即生效并投递策略变更事件
|
||||||
|
saveInp := &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "默认安全策略", LoginRateLimit: gjson.New(value), GlobalFriendEnabled: 1, MessageCurfewEnabled: 0}
|
||||||
|
tpl, tplErr := loadPolicyTemplateRow(imtenant.RegistryDB().Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||||
|
if tplErr != nil {
|
||||||
|
return gerror.Wrap(tplErr, "读取安全策略模板失败")
|
||||||
|
}
|
||||||
|
if tpl != nil {
|
||||||
|
saveInp.PolicyName = tpl["policy_name"].String()
|
||||||
|
saveInp.PasswordPolicy = gjson.New(tpl["password_policy"].String())
|
||||||
|
saveInp.SensitiveWordPolicy = gjson.New(tpl["sensitive_word_policy"].String())
|
||||||
|
saveInp.FileReviewPolicy = gjson.New(tpl["file_review_policy"].String())
|
||||||
|
saveInp.RealnamePolicy = gjson.New(tpl["realname_policy"].String())
|
||||||
|
saveInp.RiskChallengePolicy = gjson.New(tpl["risk_challenge_policy"].String())
|
||||||
|
saveInp.GlobalFriendEnabled = tpl["global_friend_enabled"].Int()
|
||||||
|
saveInp.FriendDisabledPolicy = gjson.New(tpl["friend_disabled_policy"].String())
|
||||||
|
saveInp.MessageCurfewEnabled = tpl["message_curfew_enabled"].Int()
|
||||||
|
saveInp.CurfewRules = templateCurfewRules(tpl)
|
||||||
|
}
|
||||||
|
id, saveErr := s.Save(ctx, saveInp)
|
||||||
if saveErr != nil {
|
if saveErr != nil {
|
||||||
return saveErr
|
return saveErr
|
||||||
}
|
}
|
||||||
if err = s.Publish(ctx, &sysin.ImSecurityPolicyPublishInp{Id: id, Reason: "登录失败策略首次配置"}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", id, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", id, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,32 +23,14 @@ import (
|
|||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sSysImTenant struct{}
|
type sSysImTenant struct{}
|
||||||
|
|
||||||
const (
|
|
||||||
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
|
|
||||||
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
|
|
||||||
tenantConfigMessageEncryptionEnabled = "message_encryption_enabled"
|
|
||||||
tenantConfigRealnameReviewPolicy = "realname_review_policy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// imManageRoleKey 商户级IM管理员角色标识
|
// imManageRoleKey 商户级IM管理员角色标识
|
||||||
const imManageRoleKey = "imManage"
|
const imManageRoleKey = "imManage"
|
||||||
|
|
||||||
type imTenantConfigModeration struct {
|
|
||||||
Id int64 `json:"id" orm:"id"`
|
|
||||||
TenantId int64 `json:"tenant_id" orm:"tenant_id"`
|
|
||||||
MaxUsers uint64 `json:"max_users" orm:"max_users"`
|
|
||||||
MaxGroups uint64 `json:"max_groups" orm:"max_groups"`
|
|
||||||
MaxGroupMembers uint `json:"max_group_members" orm:"max_group_members"`
|
|
||||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled" orm:"aliyun_moderation_enabled"`
|
|
||||||
LocalModerationEnabled int `json:"local_moderation_enabled" orm:"local_moderation_enabled"`
|
|
||||||
MessageEncryptionEnabled int `json:"message_encryption_enabled" orm:"message_encryption_enabled"`
|
|
||||||
RealnameReviewPolicy int `json:"realname_review_policy" orm:"realname_review_policy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSysImTenant() *sSysImTenant {
|
func NewSysImTenant() *sSysImTenant {
|
||||||
return &sSysImTenant{}
|
return &sSysImTenant{}
|
||||||
}
|
}
|
||||||
@@ -62,10 +44,6 @@ func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gd
|
|||||||
return handler.Model(imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
|
return handler.Model(imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
|
|
||||||
return imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 获取IM租户列表
|
// List 获取IM租户列表
|
||||||
func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) {
|
func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
@@ -77,7 +55,13 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
|||||||
dao.ImTenant.Columns().Code,
|
dao.ImTenant.Columns().Code,
|
||||||
dao.ImTenant.Columns().Name,
|
dao.ImTenant.Columns().Name,
|
||||||
dao.ImTenant.Columns().TenantDesc,
|
dao.ImTenant.Columns().TenantDesc,
|
||||||
dao.ImTenant.Columns().DbName,
|
dao.ImTenant.Columns().MaxUsers,
|
||||||
|
dao.ImTenant.Columns().MaxGroups,
|
||||||
|
dao.ImTenant.Columns().MaxGroupMembers,
|
||||||
|
dao.ImTenant.Columns().AliyunModerationEnabled,
|
||||||
|
dao.ImTenant.Columns().LocalModerationEnabled,
|
||||||
|
dao.ImTenant.Columns().MessageEncryptionEnabled,
|
||||||
|
dao.ImTenant.Columns().RealnameReviewPolicy,
|
||||||
dao.ImTenant.Columns().State,
|
dao.ImTenant.Columns().State,
|
||||||
dao.ImTenant.Columns().CreatedBy,
|
dao.ImTenant.Columns().CreatedBy,
|
||||||
dao.ImTenant.Columns().UpdatedBy,
|
dao.ImTenant.Columns().UpdatedBy,
|
||||||
@@ -102,9 +86,6 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
|||||||
if in.Name != "" {
|
if in.Name != "" {
|
||||||
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
|
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
|
||||||
}
|
}
|
||||||
if in.DbName != "" && isSuperTenantAdmin(ctx) {
|
|
||||||
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
|
|
||||||
}
|
|
||||||
if in.State != nil {
|
if in.State != nil {
|
||||||
mod = mod.Where(dao.ImTenant.Columns().State, *in.State)
|
mod = mod.Where(dao.ImTenant.Columns().State, *in.State)
|
||||||
}
|
}
|
||||||
@@ -112,72 +93,25 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
|||||||
mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = mod.Page(in.Page, in.PerPage).
|
orderDesc := false
|
||||||
OrderDesc(dao.ImTenant.Columns().Id).
|
for _, sorter := range in.GetSorters() {
|
||||||
Hook(hook.MemberSummary)
|
if sorter != nil && sorter.ColumnKey == "id" {
|
||||||
|
orderDesc = sorter.Order == "descend"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod = mod.Page(in.Page, in.PerPage)
|
||||||
|
if orderDesc {
|
||||||
|
mod = mod.OrderDesc(dao.ImTenant.Columns().Id)
|
||||||
|
} else {
|
||||||
|
mod = mod.OrderAsc(dao.ImTenant.Columns().Id)
|
||||||
|
}
|
||||||
|
mod = mod.Hook(hook.MemberSummary)
|
||||||
|
|
||||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||||
err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!")
|
err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(list) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.fillListConfig(ctx, list); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 业务库名仅超管可见,非超管脱敏返回
|
|
||||||
if !isSuperTenantAdmin(ctx) {
|
|
||||||
for _, item := range list {
|
|
||||||
item.DbName = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenantListModel) (err error) {
|
|
||||||
ids := make([]int64, 0, len(list))
|
|
||||||
for _, item := range list {
|
|
||||||
ids = append(ids, item.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
var configs []*imTenantConfigModeration
|
|
||||||
if err = s.configModel(ctx).
|
|
||||||
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
|
|
||||||
Scan(&configs); err != nil {
|
|
||||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
|
||||||
}
|
|
||||||
configMap := make(map[int64]*imTenantConfigModeration, len(configs))
|
|
||||||
for _, item := range configs {
|
|
||||||
configMap[item.TenantId] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
var tenants []*entity.ImTenant
|
|
||||||
if err = s.Model(ctx).
|
|
||||||
Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().DbDsn).
|
|
||||||
WhereIn(dao.ImTenant.Columns().Id, ids).
|
|
||||||
Scan(&tenants); err != nil {
|
|
||||||
return gerror.Wrap(err, "获取IM租户连接配置失败,请稍后重试!")
|
|
||||||
}
|
|
||||||
dsnMap := make(map[int64]string, len(tenants))
|
|
||||||
for _, item := range tenants {
|
|
||||||
dsnMap[item.Id] = item.DbDsn
|
|
||||||
}
|
|
||||||
|
|
||||||
super := isSuperTenantAdmin(ctx)
|
|
||||||
for _, item := range list {
|
|
||||||
if conf, ok := configMap[item.Id]; ok {
|
|
||||||
item.MaxUsers = conf.MaxUsers
|
|
||||||
item.MaxGroups = conf.MaxGroups
|
|
||||||
item.MaxGroupMembers = conf.MaxGroupMembers
|
|
||||||
item.AliyunModerationEnabled = conf.AliyunModerationEnabled
|
|
||||||
item.LocalModerationEnabled = conf.LocalModerationEnabled
|
|
||||||
item.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
|
|
||||||
item.RealnameReviewPolicy = conf.RealnameReviewPolicy
|
|
||||||
}
|
|
||||||
item.DbDsnConfigured = super && dsnMap[item.Id] != ""
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,13 +156,19 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
|||||||
super := isSuperTenantAdmin(ctx)
|
super := isSuperTenantAdmin(ctx)
|
||||||
isCreate := in.Id == 0
|
isCreate := in.Id == 0
|
||||||
tenantID := in.Id
|
tenantID := in.Id
|
||||||
|
requestedState := in.State
|
||||||
// 业务库名与独立连接串仅超管可维护,非超管提交一律强制忽略
|
if isCreate {
|
||||||
if !super {
|
if requestedState != consts.ImTenantStateActive && requestedState != consts.ImTenantStateSuspended {
|
||||||
in.DbName = ""
|
return gerror.New("新商户创建完成后的状态只能是正常或已停用")
|
||||||
in.DbDsn = ""
|
}
|
||||||
|
if err = ensureTenantIDSequence(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in.State = consts.ImTenantStateProvisioning
|
||||||
|
}
|
||||||
|
if err = validateTenantMessageEncryption(ctx, in.Id, in.MessageEncryptionEnabled); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||||
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
||||||
return
|
return
|
||||||
@@ -241,31 +181,41 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
|||||||
dao.ImTenant.Columns().Code: in.Code,
|
dao.ImTenant.Columns().Code: in.Code,
|
||||||
dao.ImTenant.Columns().Name: in.Name,
|
dao.ImTenant.Columns().Name: in.Name,
|
||||||
dao.ImTenant.Columns().TenantDesc: in.TenantDesc,
|
dao.ImTenant.Columns().TenantDesc: in.TenantDesc,
|
||||||
|
dao.ImTenant.Columns().MaxUsers: in.MaxUsers,
|
||||||
|
dao.ImTenant.Columns().MaxGroups: in.MaxGroups,
|
||||||
|
dao.ImTenant.Columns().MaxGroupMembers: in.MaxGroupMembers,
|
||||||
|
dao.ImTenant.Columns().AliyunModerationEnabled: in.AliyunModerationEnabled,
|
||||||
|
dao.ImTenant.Columns().LocalModerationEnabled: in.LocalModerationEnabled,
|
||||||
|
dao.ImTenant.Columns().MessageEncryptionEnabled: in.MessageEncryptionEnabled,
|
||||||
|
dao.ImTenant.Columns().RealnameReviewPolicy: in.RealnameReviewPolicy,
|
||||||
dao.ImTenant.Columns().State: in.State,
|
dao.ImTenant.Columns().State: in.State,
|
||||||
dao.ImTenant.Columns().UpdatedBy: userId,
|
dao.ImTenant.Columns().UpdatedBy: userId,
|
||||||
}
|
}
|
||||||
if super {
|
|
||||||
data[dao.ImTenant.Columns().DbName] = in.DbName
|
|
||||||
data[dao.ImTenant.Columns().DbDsn] = in.DbDsn
|
|
||||||
}
|
|
||||||
if _, err = s.Model(ctx).
|
if _, err = s.Model(ctx).
|
||||||
WherePri(in.Id).Data(data).Update(); err != nil {
|
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||||
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
return s.saveConfig(ctx, in, userId)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
in.Id, err = nextTenantID(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
in.CreatedBy = userId
|
in.CreatedBy = userId
|
||||||
in.UpdatedBy = userId
|
in.UpdatedBy = userId
|
||||||
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
_, err = tx.Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||||
Fields(sysin.ImTenantInsertFields{}).
|
Fields(sysin.ImTenantInsertFields{}).
|
||||||
Data(in).InsertAndGetId()
|
Data(in).Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
tenantID = id
|
tenantID = in.Id
|
||||||
in.Id = id
|
// 新建商户时复制安全策略模板为本商户初始策略(保存即生效模型)
|
||||||
return s.saveConfig(ctx, in, userId)
|
if err = copyDefaultPolicyForTenant(ctx, tx, in.Id, userId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -277,13 +227,25 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
|||||||
s.rollbackTenantCreation(ctx, tenantID)
|
s.rollbackTenantCreation(ctx, tenantID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = provisionTenantMessageTables(ctx, tenantID); err != nil {
|
||||||
|
return gerror.Wrap(err, "商户已登记为初始化中,但业务分表创建失败")
|
||||||
|
}
|
||||||
|
if err = initializeTenantWorkbench(ctx, tenantID, in.Code); err != nil {
|
||||||
|
return gerror.Wrap(err, "商户业务分表已创建,但工作台初始化失败")
|
||||||
|
}
|
||||||
|
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).WherePri(tenantID).Data(g.Map{
|
||||||
|
dao.ImTenant.Columns().State: requestedState,
|
||||||
|
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||||
|
}).Update(); err != nil {
|
||||||
|
return gerror.Wrap(err, "商户分表已创建,但更新商户状态失败")
|
||||||
|
}
|
||||||
|
in.State = requestedState
|
||||||
} else if in.AdminPassword != "" {
|
} else if in.AdminPassword != "" {
|
||||||
// 编辑商户时填写了新密码则重置管理员密码,留空不修改
|
// 编辑商户时填写了新密码则重置管理员密码,留空不修改。
|
||||||
if err = s.resetTenantAdminPassword(ctx, tenantID, in.AdminPassword); err != nil {
|
if err = s.resetTenantAdminPassword(ctx, tenantID, in.AdminPassword); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = publishImPolicyEvent(ctx, tenantID, in.Code, "tenant.features.updated", "moderation", 0)
|
_ = publishImPolicyEvent(ctx, tenantID, in.Code, "tenant.features.updated", "moderation", 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -396,12 +358,91 @@ func (s *sSysImTenant) rollbackTenantCreation(ctx context.Context, tenantId int6
|
|||||||
WherePri(tenantId).Delete(); err != nil {
|
WherePri(tenantId).Delete(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = s.configModel(ctx).
|
// 清理建户时复制的安全策略与宵禁规则,避免残留孤儿数据(商户未建成,硬删不留软删垃圾)
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).Delete()
|
if _, err = tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||||
|
Where(dao.ImSecurityPolicy.Columns().TenantId, tenantId).Unscoped().Delete(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).
|
||||||
|
Where(dao.ImMessageCurfewRules.Columns().TenantId, tenantId).Unscoped().Delete()
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyDefaultPolicyForTenant 新建商户时复制安全策略模板为本商户自己的初始策略。
|
||||||
|
// 模板未初始化时跳过(运行时由内部配置接口的默认值兜底),不阻断建户。
|
||||||
|
func copyDefaultPolicyForTenant(ctx context.Context, tx gdb.TX, tenantID, operator int64) (err error) {
|
||||||
|
tpl, err := loadPolicyTemplateRow(tx.Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "读取安全策略模板失败")
|
||||||
|
}
|
||||||
|
if tpl == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pc := dao.ImSecurityPolicy.Columns()
|
||||||
|
now := gtime.Now()
|
||||||
|
data := g.Map{
|
||||||
|
pc.TenantId: tenantID,
|
||||||
|
pc.PolicyName: tpl["policy_name"].String(),
|
||||||
|
pc.PolicyVersion: 1,
|
||||||
|
pc.PublishStatus: consts.ImSecurityPublishPublished,
|
||||||
|
pc.PasswordPolicy: tpl["password_policy"].String(),
|
||||||
|
pc.LoginRateLimit: tpl["login_rate_limit"].String(),
|
||||||
|
pc.SensitiveWordPolicy: tpl["sensitive_word_policy"].String(),
|
||||||
|
pc.FileReviewPolicy: tpl["file_review_policy"].String(),
|
||||||
|
pc.RealnamePolicy: tpl["realname_policy"].String(),
|
||||||
|
pc.RiskChallengePolicy: tpl["risk_challenge_policy"].String(),
|
||||||
|
pc.GlobalFriendEnabled: tpl["global_friend_enabled"].Int(),
|
||||||
|
pc.FriendDisabledPolicy: tpl["friend_disabled_policy"].String(),
|
||||||
|
pc.MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(),
|
||||||
|
pc.Remark: "由默认模板复制",
|
||||||
|
pc.PublishedAt: now,
|
||||||
|
pc.PublishedBy: operator,
|
||||||
|
pc.CreatedAt: now,
|
||||||
|
pc.CreatedBy: operator,
|
||||||
|
pc.UpdatedAt: now,
|
||||||
|
pc.UpdatedBy: operator,
|
||||||
|
}
|
||||||
|
result, err := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "复制安全策略模板失败")
|
||||||
|
}
|
||||||
|
policyID, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 同步复制模板宵禁规则
|
||||||
|
rc := dao.ImMessageCurfewRules.Columns()
|
||||||
|
for _, rule := range templateCurfewRules(tpl) {
|
||||||
|
ruleData := g.Map{
|
||||||
|
rc.TenantId: tenantID,
|
||||||
|
rc.PolicyId: policyID,
|
||||||
|
rc.RuleCode: rule.RuleCode,
|
||||||
|
rc.RuleName: rule.RuleName,
|
||||||
|
rc.DateType: rule.DateType,
|
||||||
|
rc.CustomDates: policyJSON(rule.CustomDates, "[]"),
|
||||||
|
rc.Weekdays: policyJSON(rule.Weekdays, "[]"),
|
||||||
|
rc.Timezone: rule.Timezone,
|
||||||
|
rc.StartTime: rule.StartTime,
|
||||||
|
rc.EndTime: rule.EndTime,
|
||||||
|
rc.Scope: rule.Scope,
|
||||||
|
rc.WhitelistUserIds: policyJSON(rule.WhitelistUserIds, "[]"),
|
||||||
|
rc.WhitelistGroupIds: policyJSON(rule.WhitelistGroupIds, "[]"),
|
||||||
|
rc.Message: rule.Message,
|
||||||
|
rc.Status: rule.Status,
|
||||||
|
rc.Sort: rule.Sort,
|
||||||
|
rc.CreatedAt: now,
|
||||||
|
rc.CreatedBy: operator,
|
||||||
|
rc.UpdatedAt: now,
|
||||||
|
rc.UpdatedBy: operator,
|
||||||
|
}
|
||||||
|
if _, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Data(ruleData).Insert(); err != nil {
|
||||||
|
return gerror.Wrap(err, "复制模板宵禁规则失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
|
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
|
||||||
mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code)
|
mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code)
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
@@ -417,10 +458,10 @@ func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImTenant) saveConfig(ctx context.Context, in *sysin.ImTenantEditInp, userId int64) (err error) {
|
func validateTenantMessageEncryption(ctx context.Context, tenantId int64, enabled int) error {
|
||||||
if in.MessageEncryptionEnabled == 1 {
|
if enabled == 1 {
|
||||||
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||||
Where("tenant_id", in.Id).
|
Where("tenant_id", tenantId).
|
||||||
Where("status", consts.ImMessageKeyStatusActive).
|
Where("status", consts.ImMessageKeyStatusActive).
|
||||||
Count()
|
Count()
|
||||||
if queryErr != nil {
|
if queryErr != nil {
|
||||||
@@ -430,61 +471,7 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, in *sysin.ImTenantEditInp
|
|||||||
return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥")
|
return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var conf *entity.ImTenantConfig
|
return nil
|
||||||
if err = s.configModel(ctx).
|
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
|
||||||
Scan(&conf); err != nil {
|
|
||||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
|
||||||
}
|
|
||||||
|
|
||||||
data := g.Map{
|
|
||||||
dao.ImTenantConfig.Columns().MaxUsers: in.MaxUsers,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroups: in.MaxGroups,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers: in.MaxGroupMembers,
|
|
||||||
tenantConfigAliyunModerationEnabled: in.AliyunModerationEnabled,
|
|
||||||
tenantConfigLocalModerationEnabled: in.LocalModerationEnabled,
|
|
||||||
tenantConfigMessageEncryptionEnabled: in.MessageEncryptionEnabled,
|
|
||||||
tenantConfigRealnameReviewPolicy: in.RealnameReviewPolicy,
|
|
||||||
}
|
|
||||||
if conf != nil && conf.Id > 0 {
|
|
||||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
|
||||||
if _, err = s.configModel(ctx).
|
|
||||||
Fields(
|
|
||||||
dao.ImTenantConfig.Columns().MaxUsers,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroups,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
|
||||||
tenantConfigAliyunModerationEnabled,
|
|
||||||
tenantConfigLocalModerationEnabled,
|
|
||||||
tenantConfigMessageEncryptionEnabled,
|
|
||||||
tenantConfigRealnameReviewPolicy,
|
|
||||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
|
||||||
).
|
|
||||||
WherePri(conf.Id).Data(data).Update(); err != nil {
|
|
||||||
return gerror.Wrap(err, "修改IM租户配置失败,请稍后重试!")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data[dao.ImTenantConfig.Columns().TenantId] = in.Id
|
|
||||||
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
|
|
||||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
|
||||||
if _, err = s.configModel(ctx).
|
|
||||||
Fields(
|
|
||||||
dao.ImTenantConfig.Columns().TenantId,
|
|
||||||
dao.ImTenantConfig.Columns().MaxUsers,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroups,
|
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
|
||||||
tenantConfigAliyunModerationEnabled,
|
|
||||||
tenantConfigLocalModerationEnabled,
|
|
||||||
tenantConfigMessageEncryptionEnabled,
|
|
||||||
tenantConfigRealnameReviewPolicy,
|
|
||||||
dao.ImTenantConfig.Columns().CreatedBy,
|
|
||||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
|
||||||
).
|
|
||||||
Data(data).Insert(); err != nil {
|
|
||||||
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// View 获取IM租户指定信息
|
// View 获取IM租户指定信息
|
||||||
@@ -492,6 +479,9 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = ensureImTenantTargetAllowed(ctx, in.Id); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||||
err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
|
err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
|
||||||
return
|
return
|
||||||
@@ -500,23 +490,6 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var conf *imTenantConfigModeration
|
|
||||||
if err = s.configModel(ctx).
|
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
|
||||||
Scan(&conf); err != nil {
|
|
||||||
err = gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if conf != nil {
|
|
||||||
res.MaxUsers = conf.MaxUsers
|
|
||||||
res.MaxGroups = conf.MaxGroups
|
|
||||||
res.MaxGroupMembers = conf.MaxGroupMembers
|
|
||||||
res.AliyunModerationEnabled = conf.AliyunModerationEnabled
|
|
||||||
res.LocalModerationEnabled = conf.LocalModerationEnabled
|
|
||||||
res.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
|
|
||||||
res.RealnameReviewPolicy = conf.RealnameReviewPolicy
|
|
||||||
}
|
|
||||||
res.DbDsnConfigured = res.DbDsn != ""
|
|
||||||
// 查询商户自动创建的管理员账号(编辑弹窗展示)
|
// 查询商户自动创建的管理员账号(编辑弹窗展示)
|
||||||
roleVal, roleErr := dao.AdminRole.Ctx(ctx).
|
roleVal, roleErr := dao.AdminRole.Ctx(ctx).
|
||||||
Fields(dao.AdminRole.Columns().Id).
|
Fields(dao.AdminRole.Columns().Id).
|
||||||
@@ -536,12 +509,6 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
|||||||
res.AdminUsername = userVal.String()
|
res.AdminUsername = userVal.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 业务库名与独立连接串仅超管可见,非超管全部脱敏
|
|
||||||
if !isSuperTenantAdmin(ctx) {
|
|
||||||
res.DbName = ""
|
|
||||||
res.DbDsn = ""
|
|
||||||
res.DbDsnConfigured = false
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +517,14 @@ func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp)
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if in.State == consts.ImTenantStateActive {
|
||||||
|
if err = provisionTenantMessageTables(ctx, in.Id); err != nil {
|
||||||
|
return gerror.Wrap(err, "启用商户前创建业务分表失败")
|
||||||
|
}
|
||||||
|
if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil {
|
||||||
|
return gerror.Wrap(err, "启用商户前初始化工作台失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||||
dao.ImTenant.Columns().State: in.State,
|
dao.ImTenant.Columns().State: in.State,
|
||||||
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||||
@@ -564,6 +539,9 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = ensureImTenantTargetAllowed(ctx, in.Id); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var tenant *entity.ImTenant
|
var tenant *entity.ImTenant
|
||||||
if err = s.Model(ctx).WherePri(in.Id).Scan(&tenant); err != nil {
|
if err = s.Model(ctx).WherePri(in.Id).Scan(&tenant); err != nil {
|
||||||
@@ -577,15 +555,8 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r
|
|||||||
Id: tenant.Id,
|
Id: tenant.Id,
|
||||||
Code: tenant.Code,
|
Code: tenant.Code,
|
||||||
Name: tenant.Name,
|
Name: tenant.Name,
|
||||||
DbName: tenant.DbName,
|
|
||||||
DbDsnConfigured: tenant.DbDsn != "",
|
|
||||||
Warnings: []string{},
|
Warnings: []string{},
|
||||||
}
|
}
|
||||||
// 业务库名与连接串信息仅超管可见,非超管全部脱敏
|
|
||||||
if !isSuperTenantAdmin(ctx) {
|
|
||||||
res.DbName = ""
|
|
||||||
res.DbDsnConfigured = false
|
|
||||||
}
|
|
||||||
|
|
||||||
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
|
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
|
||||||
if dbErr != nil {
|
if dbErr != nil {
|
||||||
@@ -601,18 +572,36 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r
|
|||||||
res.DbReachable = true
|
res.DbReachable = true
|
||||||
|
|
||||||
res.SchemaVersion = s.queryScalarString(ctx, db, "kvmeta.version", "SELECT value FROM kvmeta WHERE key_='version' LIMIT 1", &res.Warnings)
|
res.SchemaVersion = s.queryScalarString(ctx, db, "kvmeta.version", "SELECT value FROM kvmeta WHERE key_='version' LIMIT 1", &res.Warnings)
|
||||||
res.UserCount = s.queryScalarInt64(ctx, db, "im_users.total", "SELECT COUNT(*) FROM im_users", &res.Warnings)
|
tenantId := tenant.Id
|
||||||
res.ActiveUserCount = s.queryScalarInt64(ctx, db, "im_users.active", "SELECT COUNT(*) FROM im_users WHERE state=0", &res.Warnings)
|
res.UserCount = s.queryScalarInt64(ctx, db, "im_users.total", "SELECT COUNT(*) FROM im_users WHERE tenant_id=?", &res.Warnings, tenantId)
|
||||||
res.GroupCount = s.queryScalarInt64(ctx, db, "im_group_settings", "SELECT COUNT(*) FROM im_group_settings WHERE group_status<>'dissolved'", &res.Warnings)
|
res.ActiveUserCount = s.queryScalarInt64(ctx, db, "im_users.active", "SELECT COUNT(*) FROM im_users WHERE tenant_id=? AND state=0", &res.Warnings, tenantId)
|
||||||
res.DeviceCount = s.queryScalarInt64(ctx, db, "im_devices", "SELECT COUNT(*) FROM im_devices", &res.Warnings)
|
res.GroupCount = s.queryScalarInt64(ctx, db, "im_group_settings", "SELECT COUNT(*) FROM im_group_settings WHERE tenant_id=? AND group_status<>'dissolved'", &res.Warnings, tenantId)
|
||||||
res.PushTokenCount = s.queryScalarInt64(ctx, db, "im_device_push_tokens", "SELECT COUNT(*) FROM im_device_push_tokens", &res.Warnings)
|
res.DeviceCount = s.queryScalarInt64(ctx, db, "im_devices", "SELECT COUNT(*) FROM im_devices WHERE tenant_id=?", &res.Warnings, tenantId)
|
||||||
res.InviteUseCount = s.queryScalarInt64(ctx, db, "im_registration_invite_uses", "SELECT COUNT(*) FROM im_registration_invite_uses", &res.Warnings)
|
res.PushTokenCount = s.queryScalarInt64(ctx, db, "im_device_push_tokens", "SELECT COUNT(*) FROM im_device_push_tokens WHERE tenant_id=?", &res.Warnings, tenantId)
|
||||||
res.OutboxPendingCount = s.queryScalarInt64(ctx, db, "im_business_events_outbox", "SELECT COUNT(*) FROM im_business_events_outbox WHERE status='pending'", &res.Warnings)
|
res.InviteUseCount = s.queryScalarInt64(ctx, db, "im_registration_invite_uses", "SELECT COUNT(*) FROM im_registration_invite_uses WHERE tenant_id=?", &res.Warnings, tenantId)
|
||||||
|
res.OutboxPendingCount = s.queryScalarInt64(ctx, db, "im_business_events_outbox", "SELECT COUNT(*) FROM im_business_events_outbox WHERE tenant_id=? AND status='pending'", &res.Warnings, tenantId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysImTenant) queryScalarInt64(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string) int64 {
|
func ensureImTenantTargetAllowed(ctx context.Context, tenantId int64) error {
|
||||||
value, err := db.GetValue(ctx, sql)
|
if isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if tenantId <= 0 {
|
||||||
|
return gerror.New("商户ID不能为空")
|
||||||
|
}
|
||||||
|
boundTenantId := contexts.GetTenantId(ctx)
|
||||||
|
if boundTenantId <= 0 {
|
||||||
|
return gerror.New("当前管理员未关联IM租户")
|
||||||
|
}
|
||||||
|
if tenantId != boundTenantId {
|
||||||
|
return gerror.New("无权访问其它商户")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysImTenant) queryScalarInt64(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string, args ...any) int64 {
|
||||||
|
value, err := db.GetValue(ctx, sql, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*warnings = append(*warnings, label+": "+err.Error())
|
*warnings = append(*warnings, label+": "+err.Error())
|
||||||
return 0
|
return 0
|
||||||
@@ -687,6 +676,12 @@ func (s *sSysImTenant) Restore(ctx context.Context, in *sysin.ImTenantLifecycleI
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = provisionTenantMessageTables(ctx, in.Id); err != nil {
|
||||||
|
return gerror.Wrap(err, "恢复商户前创建业务分表失败")
|
||||||
|
}
|
||||||
|
if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil {
|
||||||
|
return gerror.Wrap(err, "恢复商户前初始化工作台失败")
|
||||||
|
}
|
||||||
if _, err = s.Model(ctx).
|
if _, err = s.Model(ctx).
|
||||||
WherePri(in.Id).
|
WherePri(in.Id).
|
||||||
WhereIn(dao.ImTenant.Columns().State, []int{consts.ImTenantStateSuspended, consts.ImTenantStateDeleting}).
|
WhereIn(dao.ImTenant.Columns().State, []int{consts.ImTenantStateSuspended, consts.ImTenantStateDeleting}).
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// Package sys
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sSysImTenantApplication struct{}
|
||||||
|
|
||||||
|
func NewSysImTenantApplication() *sSysImTenantApplication {
|
||||||
|
return &sSysImTenantApplication{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
service.RegisterSysImTenantApplication(NewSysImTenantApplication())
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取企业注册申请列表
|
||||||
|
func (s *sSysImTenantApplication) List(ctx context.Context, in *sysin.ImTenantApplicationListInp) (list []*sysin.ImTenantApplicationListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
where, args := s.buildWhere(in)
|
||||||
|
totalValue, err := g.DB().GetValue(ctx, "SELECT COUNT(*) FROM im_tenant_applications "+where, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "获取企业注册申请总数失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
totalCount = totalValue.Int()
|
||||||
|
|
||||||
|
page, perPage := normalizePage(in.Page, in.PerPage)
|
||||||
|
query := `SELECT id,application_id,credit_code,enterprise_email,license_file_id,tenant_code_hint,
|
||||||
|
status,estimated_review_hour,submitted_at,created_at,updated_at
|
||||||
|
FROM im_tenant_applications ` + where + ` ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?`
|
||||||
|
queryArgs := append(append([]any{}, args...), perPage, (page-1)*perPage)
|
||||||
|
rows, err := g.DB().GetAll(ctx, query, queryArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "获取企业注册申请列表失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if err = rows.Structs(&list); err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "解析企业注册申请列表失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysImTenantApplication) buildWhere(in *sysin.ImTenantApplicationListInp) (string, []any) {
|
||||||
|
parts := []string{"1=1"}
|
||||||
|
args := []any{}
|
||||||
|
if in.ApplicationId != "" {
|
||||||
|
parts = append(parts, "application_id LIKE ?")
|
||||||
|
args = append(args, "%"+in.ApplicationId+"%")
|
||||||
|
}
|
||||||
|
if in.CreditCode != "" {
|
||||||
|
parts = append(parts, "credit_code LIKE ?")
|
||||||
|
args = append(args, "%"+in.CreditCode+"%")
|
||||||
|
}
|
||||||
|
if in.EnterpriseEmail != "" {
|
||||||
|
parts = append(parts, "enterprise_email LIKE ?")
|
||||||
|
args = append(args, "%"+in.EnterpriseEmail+"%")
|
||||||
|
}
|
||||||
|
if in.TenantCodeHint != "" {
|
||||||
|
parts = append(parts, "tenant_code_hint LIKE ?")
|
||||||
|
args = append(args, "%"+in.TenantCodeHint+"%")
|
||||||
|
}
|
||||||
|
if in.Status != "" {
|
||||||
|
parts = append(parts, "status=?")
|
||||||
|
args = append(args, in.Status)
|
||||||
|
}
|
||||||
|
if in.Keyword != "" {
|
||||||
|
like := "%" + in.Keyword + "%"
|
||||||
|
parts = append(parts, "(application_id LIKE ? OR credit_code LIKE ? OR enterprise_email LIKE ? OR tenant_code_hint LIKE ?)")
|
||||||
|
args = append(args, like, like, like, like)
|
||||||
|
}
|
||||||
|
if len(in.CreatedAt) == 2 {
|
||||||
|
parts = append(parts, "created_at BETWEEN ? AND ?")
|
||||||
|
args = append(args, in.CreatedAt[0], in.CreatedAt[1])
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(parts, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
// View 获取企业注册申请指定信息
|
||||||
|
func (s *sSysImTenantApplication) View(ctx context.Context, in *sysin.ImTenantApplicationViewInp) (res *sysin.ImTenantApplicationViewModel, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT id,application_id,credit_code,enterprise_email,license_file_id,tenant_code_hint,
|
||||||
|
CAST(client_context AS CHAR) client_context,idempotency_key,status,estimated_review_hour,
|
||||||
|
submitted_at,created_at,updated_at FROM im_tenant_applications WHERE id=? LIMIT 1`
|
||||||
|
row, err := g.DB().GetOne(ctx, query, in.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取企业注册申请信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if row.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err = row.Struct(&res); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "解析企业注册申请信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.LicenseFile, err = firstImPublicUpload(ctx, res.LicenseFileId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePage(page, perPage int) (int, int) {
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if perPage <= 0 {
|
||||||
|
perPage = 20
|
||||||
|
}
|
||||||
|
return page, perPage
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Package sys
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sSysImTenantEntryFeedback struct{}
|
||||||
|
|
||||||
|
func NewSysImTenantEntryFeedback() *sSysImTenantEntryFeedback {
|
||||||
|
return &sSysImTenantEntryFeedback{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
service.RegisterSysImTenantEntryFeedback(NewSysImTenantEntryFeedback())
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取企业入口反馈列表
|
||||||
|
func (s *sSysImTenantEntryFeedback) List(ctx context.Context, in *sysin.ImTenantEntryFeedbackListInp) (list []*sysin.ImTenantEntryFeedbackListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
where, args := s.buildWhere(in)
|
||||||
|
totalValue, err := g.DB().GetValue(ctx, "SELECT COUNT(*) FROM im_tenant_entry_feedbacks "+where, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "获取企业入口反馈总数失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
totalCount = totalValue.Int()
|
||||||
|
|
||||||
|
page, perPage := normalizePage(in.Page, in.PerPage)
|
||||||
|
query := `SELECT id,feedback_id,description,contact,tenant_code_hint,status,submitted_at,created_at,updated_at
|
||||||
|
FROM im_tenant_entry_feedbacks ` + where + ` ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?`
|
||||||
|
queryArgs := append(append([]any{}, args...), perPage, (page-1)*perPage)
|
||||||
|
rows, err := g.DB().GetAll(ctx, query, queryArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "获取企业入口反馈列表失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if err = rows.Structs(&list); err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "解析企业入口反馈列表失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysImTenantEntryFeedback) buildWhere(in *sysin.ImTenantEntryFeedbackListInp) (string, []any) {
|
||||||
|
parts := []string{"1=1"}
|
||||||
|
args := []any{}
|
||||||
|
if in.FeedbackId != "" {
|
||||||
|
parts = append(parts, "feedback_id LIKE ?")
|
||||||
|
args = append(args, "%"+in.FeedbackId+"%")
|
||||||
|
}
|
||||||
|
if in.Contact != "" {
|
||||||
|
parts = append(parts, "contact LIKE ?")
|
||||||
|
args = append(args, "%"+in.Contact+"%")
|
||||||
|
}
|
||||||
|
if in.TenantCodeHint != "" {
|
||||||
|
parts = append(parts, "tenant_code_hint LIKE ?")
|
||||||
|
args = append(args, "%"+in.TenantCodeHint+"%")
|
||||||
|
}
|
||||||
|
if in.Status != "" {
|
||||||
|
parts = append(parts, "status=?")
|
||||||
|
args = append(args, in.Status)
|
||||||
|
}
|
||||||
|
if in.Keyword != "" {
|
||||||
|
like := "%" + in.Keyword + "%"
|
||||||
|
parts = append(parts, "(feedback_id LIKE ? OR description LIKE ? OR contact LIKE ? OR tenant_code_hint LIKE ?)")
|
||||||
|
args = append(args, like, like, like, like)
|
||||||
|
}
|
||||||
|
if len(in.CreatedAt) == 2 {
|
||||||
|
parts = append(parts, "created_at BETWEEN ? AND ?")
|
||||||
|
args = append(args, in.CreatedAt[0], in.CreatedAt[1])
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(parts, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
// View 获取企业入口反馈指定信息
|
||||||
|
func (s *sSysImTenantEntryFeedback) View(ctx context.Context, in *sysin.ImTenantEntryFeedbackViewInp) (res *sysin.ImTenantEntryFeedbackViewModel, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT id,feedback_id,description,contact,tenant_code_hint,
|
||||||
|
CAST(screenshot_file_ids AS CHAR) screenshot_file_ids,CAST(client_context AS CHAR) client_context,
|
||||||
|
idempotency_key,status,submitted_at,created_at,updated_at
|
||||||
|
FROM im_tenant_entry_feedbacks WHERE id=? LIMIT 1`
|
||||||
|
row, err := g.DB().GetOne(ctx, query, in.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "获取企业入口反馈信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if row.IsEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err = row.Struct(&res); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "解析企业入口反馈信息失败,请稍后重试!")
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.ScreenshotFiles, err = loadImPublicUploads(ctx, parseJSONStringSlice(res.ScreenshotFileIds))
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"hotgo/internal/library/imtenant"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
const imTenantIDSequenceTable = "im_tenant_id_sequence"
|
||||||
|
|
||||||
|
func ensureTenantIDSequence(ctx context.Context) error {
|
||||||
|
db := imtenant.RegistryDB()
|
||||||
|
if _, err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS im_tenant_id_sequence (
|
||||||
|
sequence_name VARCHAR(32) NOT NULL,
|
||||||
|
next_id BIGINT NOT NULL,
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(sequence_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`); err != nil {
|
||||||
|
return gerror.Wrap(err, "初始化商户编号序列失败")
|
||||||
|
}
|
||||||
|
_, err := db.Exec(ctx, `INSERT INTO im_tenant_id_sequence(sequence_name,next_id)
|
||||||
|
SELECT 'tenant',COALESCE(MAX(id),0)+1 FROM im_tenant
|
||||||
|
ON DUPLICATE KEY UPDATE next_id=GREATEST(next_id,VALUES(next_id))`)
|
||||||
|
return gerror.Wrap(err, "校准商户编号序列失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextTenantID(tx gdb.TX) (int64, error) {
|
||||||
|
value, err := tx.GetValue("SELECT next_id FROM " + imTenantIDSequenceTable + " WHERE sequence_name='tenant' FOR UPDATE")
|
||||||
|
if err != nil {
|
||||||
|
return 0, gerror.Wrap(err, "锁定商户编号序列失败")
|
||||||
|
}
|
||||||
|
id := value.Int64()
|
||||||
|
if id <= 0 {
|
||||||
|
return 0, gerror.New("商户编号序列不正确")
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("UPDATE "+imTenantIDSequenceTable+" SET next_id=? WHERE sequence_name='tenant'", id+1); err != nil {
|
||||||
|
return 0, gerror.Wrap(err, "更新商户编号序列失败")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tenantTablePrefix(tenantID int64) string {
|
||||||
|
return fmt.Sprintf("im_%03d_", tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func provisionTenantMessageTables(ctx context.Context, tenantID int64) error {
|
||||||
|
if tenantID <= 0 {
|
||||||
|
return gerror.New("商户ID不能为空")
|
||||||
|
}
|
||||||
|
db, err := imtenant.SharedDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prefix := tenantTablePrefix(tenantID)
|
||||||
|
baseTables := [][2]string{
|
||||||
|
{prefix + "friendships", "im_friendships"},
|
||||||
|
{prefix + "messages", "im_messages"},
|
||||||
|
{prefix + "topics", "im_topics"},
|
||||||
|
{prefix + "group_members", "im_group_members"},
|
||||||
|
{prefix + "subscriptions", "im_subscriptions"},
|
||||||
|
{prefix + "system_notifications", "im_system_notifications"},
|
||||||
|
}
|
||||||
|
for _, pair := range baseTables {
|
||||||
|
if _, err = db.Exec(ctx, fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` LIKE `%s`", pair[0], pair[1])); err != nil {
|
||||||
|
return gerror.Wrapf(err, "创建商户业务表 %s 失败", pair[0])
|
||||||
|
}
|
||||||
|
if _, err = db.Exec(ctx, fmt.Sprintf("ALTER TABLE `%s` MODIFY tenant_id BIGINT NOT NULL DEFAULT %d COMMENT '商户ID'", pair[0], tenantID)); err != nil {
|
||||||
|
return gerror.Wrapf(err, "设置商户业务表 %s 默认商户失败", pair[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first := time.Date(time.Now().UTC().Year(), time.Now().UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
for _, offset := range []int{-1, 0, 1, 2} {
|
||||||
|
start := first.AddDate(0, offset, 0)
|
||||||
|
month := start.Format("200601")
|
||||||
|
table := prefix + "group_messages_" + month
|
||||||
|
if err = createTenantGroupMessageTable(ctx, db, table, tenantID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status := "prepared"
|
||||||
|
if offset == -1 {
|
||||||
|
status = "sealing"
|
||||||
|
} else if offset == 0 {
|
||||||
|
status = "writable"
|
||||||
|
}
|
||||||
|
if _, err = db.Exec(ctx, `INSERT INTO im_group_message_shards
|
||||||
|
(tenant_id,shard_month,table_name,shard_start,shard_end,status)
|
||||||
|
VALUES(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE table_name=VALUES(table_name),
|
||||||
|
shard_start=VALUES(shard_start),shard_end=VALUES(shard_end),
|
||||||
|
status=IF(status IN ('sealed','archived'),status,VALUES(status))`,
|
||||||
|
tenantID, month, table, start, start.AddDate(0, 1, 0), status); err != nil {
|
||||||
|
return gerror.Wrapf(err, "登记商户群消息月表 %s 失败", table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTenantGroupMessageTable(ctx context.Context, db gdb.DB, table string, tenantID int64) error {
|
||||||
|
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT %d, message_id BIGINT NOT NULL, topic VARCHAR(64) NOT NULL, group_id BIGINT NOT NULL,
|
||||||
|
seqid BIGINT NOT NULL, from_user BIGINT NOT NULL, client_msg_id VARCHAR(64),
|
||||||
|
msg_type VARCHAR(32) NOT NULL DEFAULT 'text', status VARCHAR(32) NOT NULL DEFAULT 'normal',
|
||||||
|
content JSON, content_nonce VARBINARY(12), key_version INT NOT NULL DEFAULT 0,
|
||||||
|
head JSON, mentioned_user_ids JSON, mention_all TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
reply_seqid BIGINT, root_seqid BIGINT, file_count INT NOT NULL DEFAULT 0,
|
||||||
|
content_size INT NOT NULL DEFAULT 0, has_risk TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass', version INT NOT NULL DEFAULT 1,
|
||||||
|
recalled_at DATETIME(3), recalled_by BIGINT, deleted_at DATETIME(3), expires_at DATETIME(3),
|
||||||
|
created_at DATETIME(3) NOT NULL, updated_at DATETIME(3) NOT NULL,
|
||||||
|
PRIMARY KEY(message_id), UNIQUE INDEX uk_group_seq(group_id, seqid),
|
||||||
|
UNIQUE INDEX uk_topic_seq(topic, seqid), UNIQUE INDEX uk_sender_client(topic, from_user, client_msg_id),
|
||||||
|
INDEX idx_group_status_seq(group_id, status, seqid), INDEX idx_topic_created(topic, created_at),
|
||||||
|
INDEX idx_topic_from_seq(topic, from_user, seqid), INDEX idx_audit_created(audit_status, created_at),
|
||||||
|
INDEX idx_expires(expires_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, table, tenantID)
|
||||||
|
if _, err := db.Exec(ctx, ddl); err != nil {
|
||||||
|
return gerror.Wrapf(err, "创建商户群消息月表 %s 失败", table)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/dao"
|
||||||
|
"hotgo/internal/library/hgorm/handler"
|
||||||
|
"hotgo/internal/library/imworkbench"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeTenantWorkbench(ctx context.Context, tenantID int64, tenantCode string) error {
|
||||||
|
if tenantID <= 0 {
|
||||||
|
return gerror.New("商户ID不能为空")
|
||||||
|
}
|
||||||
|
if tenantCode == "" {
|
||||||
|
value, err := NewSysImTenant().Model(ctx, &handler.Option{FilterAuth: false}).
|
||||||
|
Fields(dao.ImTenant.Columns().Code).
|
||||||
|
WherePri(tenantID).
|
||||||
|
Value()
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "查询商户编码失败")
|
||||||
|
}
|
||||||
|
tenantCode = value.String()
|
||||||
|
if tenantCode == "" {
|
||||||
|
return gerror.New("商户不存在")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
}
|
||||||
|
if err := imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/tenants/initialize", g.Map{
|
||||||
|
"tenant_id": tenantID,
|
||||||
|
"tenant_code": tenantCode,
|
||||||
|
}, tenantID, tenantCode, &result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.TenantID != tenantID {
|
||||||
|
return gerror.New("工作台初始化返回的商户ID不一致")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -861,7 +861,7 @@ func updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, user
|
|||||||
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("user-status:%d:%d:%d:%s", scope.TenantId, userId, state, requestId)))
|
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("user-status:%d:%d:%d:%s", scope.TenantId, userId, state, requestId)))
|
||||||
eventKey := hex.EncodeToString(eventKeyBytes[:])
|
eventKey := hex.EncodeToString(eventKeyBytes[:])
|
||||||
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||||
current, txErr := tx.GetValue("SELECT state FROM im_users WHERE id=? LIMIT 1", userId)
|
current, txErr := tx.GetValue("SELECT state FROM im_users WHERE tenant_id=? AND id=? LIMIT 1", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -874,17 +874,17 @@ func updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, user
|
|||||||
if current.Int() == state {
|
if current.Int() == state {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, txErr = tx.Exec("UPDATE im_users SET state=?, updatedat=? WHERE id=?", state, now, userId); txErr != nil {
|
if _, txErr = tx.Exec("UPDATE im_users SET state=?, updatedat=? WHERE tenant_id=? AND id=?", state, now, scope.TenantId, userId); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
||||||
(id,user_id,auth_version,created_at,updated_at)
|
(tenant_id,id,user_id,auth_version,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?)
|
VALUES(?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
|
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
|
||||||
securityId, userId, 2, now, now); txErr != nil {
|
scope.TenantId, securityId, userId, 2, now, now); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId)
|
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -892,9 +892,9 @@ func updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, user
|
|||||||
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "state": state, "auth_version": version.Int(),
|
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "state": state, "auth_version": version.Int(),
|
||||||
})
|
})
|
||||||
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
||||||
(id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
(tenant_id,id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
||||||
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user",
|
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, scope.TenantId, eventId, eventKey, "user",
|
||||||
fmt.Sprintf("%d", userId), 0, "security.user_status_changed", string(payload), now, now)
|
fmt.Sprintf("%d", userId), 0, "security.user_status_changed", string(payload), now, now)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
@@ -920,7 +920,7 @@ func revokeIMUserSessions(ctx context.Context, db gdb.DB, scope imAdminScope, us
|
|||||||
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%d:%s", eventType, scope.TenantId, userId, requestId)))
|
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%d:%s", eventType, scope.TenantId, userId, requestId)))
|
||||||
eventKey := hex.EncodeToString(eventKeyBytes[:])
|
eventKey := hex.EncodeToString(eventKeyBytes[:])
|
||||||
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||||
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId)
|
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE tenant_id=? AND id=? LIMIT 1)", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -928,13 +928,13 @@ func revokeIMUserSessions(ctx context.Context, db gdb.DB, scope imAdminScope, us
|
|||||||
return gerror.New("IM用户不存在")
|
return gerror.New("IM用户不存在")
|
||||||
}
|
}
|
||||||
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
||||||
(id,user_id,auth_version,created_at,updated_at)
|
(tenant_id,id,user_id,auth_version,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?)
|
VALUES(?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
|
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
|
||||||
securityId, userId, 2, now, now); txErr != nil {
|
scope.TenantId, securityId, userId, 2, now, now); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId)
|
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -942,9 +942,9 @@ func revokeIMUserSessions(ctx context.Context, db gdb.DB, scope imAdminScope, us
|
|||||||
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "auth_version": version.Int(),
|
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "auth_version": version.Int(),
|
||||||
})
|
})
|
||||||
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
||||||
(id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
(tenant_id,id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
||||||
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user",
|
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, scope.TenantId, eventId, eventKey, "user",
|
||||||
fmt.Sprintf("%d", userId), 0, eventType, string(payload), now, now)
|
fmt.Sprintf("%d", userId), 0, eventType, string(payload), now, now)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
@@ -977,14 +977,14 @@ func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, use
|
|||||||
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId,
|
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId,
|
||||||
})
|
})
|
||||||
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||||
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId)
|
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE tenant_id=? AND id=? LIMIT 1)", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
if !exists.Bool() {
|
if !exists.Bool() {
|
||||||
return gerror.New("IM用户不存在")
|
return gerror.New("IM用户不存在")
|
||||||
}
|
}
|
||||||
result, txErr := tx.Exec("UPDATE im_auth SET secret=? WHERE userid=?", passwordHash, userId)
|
result, txErr := tx.Exec("UPDATE im_auth SET secret=? WHERE tenant_id=? AND userid=?", passwordHash, scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -992,14 +992,14 @@ func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, use
|
|||||||
return gerror.New("用户认证记录不存在")
|
return gerror.New("用户认证记录不存在")
|
||||||
}
|
}
|
||||||
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
||||||
(id,user_id,auth_version,password_updated_at,created_at,updated_at)
|
(tenant_id,id,user_id,auth_version,password_updated_at,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?,?)
|
VALUES(?,?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,
|
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,
|
||||||
password_updated_at=VALUES(password_updated_at),updated_at=VALUES(updated_at)`,
|
password_updated_at=VALUES(password_updated_at),updated_at=VALUES(updated_at)`,
|
||||||
securityId, userId, 2, now, now, now); txErr != nil {
|
scope.TenantId, securityId, userId, 2, now, now, now); txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId)
|
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?", scope.TenantId, userId)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
@@ -1008,9 +1008,9 @@ func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, use
|
|||||||
eventPayload["auth_version"] = version.Int()
|
eventPayload["auth_version"] = version.Int()
|
||||||
payload, _ = json.Marshal(eventPayload)
|
payload, _ = json.Marshal(eventPayload)
|
||||||
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
||||||
(id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
(tenant_id,id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
||||||
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user",
|
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, scope.TenantId, eventId, eventKey, "user",
|
||||||
fmt.Sprintf("%d", userId), 0, "security.password_reset", string(payload), now, now)
|
fmt.Sprintf("%d", userId), 0, "security.password_reset", string(payload), now, now)
|
||||||
return txErr
|
return txErr
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (s *sSysImWorkbench) List(ctx context.Context, in *sysin.ImWorkbenchAppList
|
|||||||
values.Set("enabled", intToString(*in.Enabled))
|
values.Set("enabled", intToString(*in.Enabled))
|
||||||
}
|
}
|
||||||
var page imworkbench.Page[sysin.ImWorkbenchAppModel]
|
var page imworkbench.Page[sysin.ImWorkbenchAppModel]
|
||||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/apps", values, &page); err != nil {
|
if err = imworkbench.DefaultClient(ctx).GetForTenant(ctx, "/internal/admin/workbench/apps", values, tenant.Id, tenant.Code, &page); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
list = make([]*sysin.ImWorkbenchAppModel, 0, len(page.Items))
|
list = make([]*sysin.ImWorkbenchAppModel, 0, len(page.Items))
|
||||||
@@ -78,7 +78,7 @@ func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEdit
|
|||||||
action = "im_workbench_app_update"
|
action = "im_workbench_app_update"
|
||||||
}
|
}
|
||||||
var res sysin.ImWorkbenchAppModel
|
var res sysin.ImWorkbenchAppModel
|
||||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps", data, &res); err != nil {
|
if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps", data, tenant.Id, tenant.Code, &res); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if targetId <= 0 {
|
if targetId <= 0 {
|
||||||
@@ -98,7 +98,7 @@ func (s *sSysImWorkbench) Delete(ctx context.Context, in *sysin.ImWorkbenchAppDe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, nil); err != nil {
|
if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, tenant.Id, tenant.Code, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
|
||||||
@@ -120,7 +120,7 @@ func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppR
|
|||||||
"sort_order": item.SortOrder,
|
"sort_order": item.SortOrder,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/reorder", g.Map{"items": items}, nil); err != nil {
|
if err = imworkbench.DefaultClient(ctx).PostForTenant(ctx, "/internal/admin/workbench/apps/reorder", g.Map{"items": items}, tenant.Id, tenant.Code, nil); err != nil {
|
||||||
if err.Error() == "" {
|
if err.Error() == "" {
|
||||||
return gerror.New("调整IM工作台模块排序失败,请稍后重试!")
|
return gerror.New("调整IM工作台模块排序失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
|
||||||
|
"hotgo/internal/library/contexts"
|
||||||
|
"hotgo/internal/library/maoyuadmin"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 毛隅内容治理:概览、动态/评论审核、举报、操作日志。
|
||||||
|
// 与养成内容治理同一套页面协议,业务数据经 maoyu-server 内部接口处理,Manager 不直连毛隅业务库。
|
||||||
|
|
||||||
|
func init() { service.RegisterSysMaoyuContent(NewSysMaoyuContent()) }
|
||||||
|
|
||||||
|
type sSysMaoyuContent struct{}
|
||||||
|
|
||||||
|
func NewSysMaoyuContent() *sSysMaoyuContent { return &sSysMaoyuContent{} }
|
||||||
|
|
||||||
|
// maoyuAdminScope 从管理员登录态构造毛隅内部接口调用范围。
|
||||||
|
func maoyuAdminScope(ctx context.Context) (maoyuadmin.Scope, error) {
|
||||||
|
tenant, err := resolveImAdminTenant(ctx, cultivateTenantID(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return maoyuadmin.Scope{}, err
|
||||||
|
}
|
||||||
|
operatorID := contexts.GetUserId(ctx)
|
||||||
|
if operatorID <= 0 {
|
||||||
|
return maoyuadmin.Scope{}, gerror.New("当前管理员身份无效")
|
||||||
|
}
|
||||||
|
return maoyuadmin.Scope{
|
||||||
|
TenantID: tenant.Id, TenantCode: tenant.Code, OperatorID: operatorID,
|
||||||
|
RoleKey: contexts.GetRoleKey(ctx), RequestID: gctx.CtxId(ctx),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) client(ctx context.Context) *maoyuadmin.Client {
|
||||||
|
return maoyuadmin.DefaultClient(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) Dashboard(ctx context.Context, _ *sysin.MaoyuDashboardInp) (*sysin.MaoyuDashboardModel, error) {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.MaoyuDashboardModel)
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, "/dashboard", nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) 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
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
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", strings.TrimSpace(in.Keyword))
|
||||||
|
setTimeRange(values, in.CreatedAt)
|
||||||
|
var page maoyuadmin.Page[*sysin.CultivateContentListModel]
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, "/content-reviews", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.CultivateContentListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.CultivateContentViewModel)
|
||||||
|
path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id)
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, path, nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := "/content-reviews/" + url.PathEscape(in.ObjectType) + "/" + url.PathEscape(in.Id) + "/actions"
|
||||||
|
return s.client(ctx).Post(ctx, scope, path, map[string]any{"action": in.Action, "reason": strings.TrimSpace(in.Reason)}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.CultivateContentBatchReviewModel)
|
||||||
|
err = s.client(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 *sSysMaoyuContent) 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
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
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 maoyuadmin.Page[*sysin.CultivateReportListModel]
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, "/reports", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.CultivateReportListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.CultivateReportViewModel)
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, "/reports/"+url.PathEscape(in.Id), nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error {
|
||||||
|
if err := in.Filter(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.client(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 *sSysMaoyuContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
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 maoyuadmin.Page[*sysin.CultivateOperationLogListModel]
|
||||||
|
if err = s.client(ctx).Get(ctx, scope, "/operation-logs", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.CultivateOperationLogListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hotgo/internal/library/maoyuadmin"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
|
"hotgo/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 毛隅萌宠管理:互动动作配置(新增/编辑/启用停用)、宠物档案(只读)、场景生成任务(只读)、互动通知(只读)。
|
||||||
|
|
||||||
|
func init() { service.RegisterSysMaoyuPet(NewSysMaoyuPet()) }
|
||||||
|
|
||||||
|
type sSysMaoyuPet struct{}
|
||||||
|
|
||||||
|
func NewSysMaoyuPet() *sSysMaoyuPet { return &sSysMaoyuPet{} }
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) ActionConfigList(ctx context.Context, in *sysin.MaoyuActionConfigListInp) (list []*sysin.MaoyuActionConfigListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
normalizeCultivatePage(&in.PageReq)
|
||||||
|
values := cultivatePageValues(in.Page, in.PerPage)
|
||||||
|
cultivateSetQuery(values, "keyword", in.Keyword)
|
||||||
|
cultivateSetQuery(values, "status", in.Status)
|
||||||
|
cultivateSetQuery(values, "version", in.Version)
|
||||||
|
setTimeRange(values, in.CreatedAt)
|
||||||
|
var page maoyuadmin.Page[*sysin.MaoyuActionConfigListModel]
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/action-configs", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.MaoyuActionConfigListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) ActionConfigView(ctx context.Context, in *sysin.MaoyuActionConfigViewInp) (*sysin.MaoyuActionConfigViewModel, error) {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.MaoyuActionConfigViewModel)
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/action-configs/"+url.PathEscape(in.Id), nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) ActionConfigEdit(ctx context.Context, in *sysin.MaoyuActionConfigEditInp) error {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := map[string]any{
|
||||||
|
"action_name": in.ActionName, "daily_limit": in.DailyLimit, "cooldown_seconds": in.CooldownSeconds,
|
||||||
|
"mutex_group": in.MutexGroup, "asset_key": in.AssetKey, "effect_config": in.EffectConfig, "sort_no": in.SortNo,
|
||||||
|
}
|
||||||
|
if id := strings.TrimSpace(in.Id); id != "" {
|
||||||
|
return maoyuadmin.DefaultClient(ctx).Put(ctx, scope, "/action-configs/"+url.PathEscape(id), body, nil)
|
||||||
|
}
|
||||||
|
body["action_code"] = in.ActionCode
|
||||||
|
return maoyuadmin.DefaultClient(ctx).Post(ctx, scope, "/action-configs", body, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) ActionConfigStatus(ctx context.Context, in *sysin.MaoyuActionConfigStatusInp) error {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return maoyuadmin.DefaultClient(ctx).Post(ctx, scope, "/action-configs/"+url.PathEscape(in.Id)+"/status", map[string]any{
|
||||||
|
"reason": strings.TrimSpace(in.Reason),
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) PetList(ctx context.Context, in *sysin.MaoyuPetListInp) (list []*sysin.MaoyuPetListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
normalizeCultivatePage(&in.PageReq)
|
||||||
|
values := cultivatePageValues(in.Page, in.PerPage)
|
||||||
|
cultivateSetQuery(values, "user_id", in.UserId)
|
||||||
|
cultivateSetQuery(values, "pet_type", in.PetType)
|
||||||
|
cultivateSetQuery(values, "status", in.Status)
|
||||||
|
cultivateSetQuery(values, "keyword", in.Keyword)
|
||||||
|
setTimeRange(values, in.CreatedAt)
|
||||||
|
var page maoyuadmin.Page[*sysin.MaoyuPetListModel]
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/pets", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.MaoyuPetListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) PetView(ctx context.Context, in *sysin.MaoyuPetViewInp) (*sysin.MaoyuPetViewModel, error) {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.MaoyuPetViewModel)
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/pets/"+url.PathEscape(in.Id), nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) JobList(ctx context.Context, in *sysin.MaoyuJobListInp) (list []*sysin.MaoyuJobListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
normalizeCultivatePage(&in.PageReq)
|
||||||
|
values := cultivatePageValues(in.Page, in.PerPage)
|
||||||
|
cultivateSetQuery(values, "pet_id", in.PetId)
|
||||||
|
cultivateSetQuery(values, "user_id", in.UserId)
|
||||||
|
cultivateSetQuery(values, "status", in.Status)
|
||||||
|
cultivateSetQuery(values, "error_code", in.ErrorCode)
|
||||||
|
setTimeRange(values, in.CreatedAt)
|
||||||
|
var page maoyuadmin.Page[*sysin.MaoyuJobListModel]
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/generation-jobs", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.MaoyuJobListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) JobView(ctx context.Context, in *sysin.MaoyuJobViewInp) (*sysin.MaoyuJobViewModel, error) {
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(sysin.MaoyuJobViewModel)
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/generation-jobs/"+url.PathEscape(in.Id), nil, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sSysMaoyuPet) NoticeList(ctx context.Context, in *sysin.MaoyuNoticeListInp) (list []*sysin.MaoyuNoticeListModel, totalCount int, err error) {
|
||||||
|
if err = in.Filter(ctx); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
scope, err := maoyuAdminScope(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
normalizeCultivatePage(&in.PageReq)
|
||||||
|
values := cultivatePageValues(in.Page, in.PerPage)
|
||||||
|
cultivateSetQuery(values, "recipient_id", in.RecipientId)
|
||||||
|
cultivateSetQuery(values, "actor_id", in.ActorId)
|
||||||
|
cultivateSetQuery(values, "event_type", in.EventType)
|
||||||
|
cultivateSetQuery(values, "status", in.Status)
|
||||||
|
cultivateSetQuery(values, "post_id", in.PostId)
|
||||||
|
setTimeRange(values, in.CreatedAt)
|
||||||
|
var page maoyuadmin.Page[*sysin.MaoyuNoticeListModel]
|
||||||
|
if err = maoyuadmin.DefaultClient(ctx).Get(ctx, scope, "/interaction-notices", values, &page); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if page.Items == nil {
|
||||||
|
page.Items = []*sysin.MaoyuNoticeListModel{}
|
||||||
|
}
|
||||||
|
return page.Items, page.Total, nil
|
||||||
|
}
|
||||||
@@ -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 //
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,17 @@ type ImTenant struct {
|
|||||||
Code any //
|
Code any //
|
||||||
Name any //
|
Name any //
|
||||||
TenantDesc any //
|
TenantDesc any //
|
||||||
DbName any //
|
MaxUsers any // 0表示ä¸é™åˆ¶
|
||||||
DbDsn any //
|
MaxGroups any // 0表示ä¸é™åˆ¶
|
||||||
|
MaxGroupMembers any // 0表示ä¸é™åˆ¶
|
||||||
|
RealnameReviewPolicy any // 实åå®¡æ ¸ç–略:0自动通过,1è½¬äººå·¥å®¡æ ¸
|
||||||
|
AliyunModerationEnabled any // 是å¦å¯ç”¨é˜¿é‡Œäº‘å†…å®¹å®¡æ ¸
|
||||||
|
LocalModerationEnabled any // 是å¦å¯ç”¨æœ¬åœ°å†…å®¹å®¡æ ¸
|
||||||
|
MessageEncryptionEnabled any // 是å¦å¯ç”¨æ¶ˆæ¯åР坆
|
||||||
|
DefaultPolicyGroupId any // 默认用户ç–略组
|
||||||
|
SecurityPolicyVersion any // å·²å‘布安全ç–略版本
|
||||||
|
GlobalFriendEnabled any // å…¨å±€åŠ å¥½å‹å¼€å…³
|
||||||
|
MessageCurfewEnabled any // 消æ¯å®µç¦å¼€å…³
|
||||||
State any //
|
State any //
|
||||||
CreatedAt *gtime.Time //
|
CreatedAt *gtime.Time //
|
||||||
CreatedBy any //
|
CreatedBy any //
|
||||||
|
|||||||
@@ -1,28 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImTenantConfig is the golang structure of table im_tenant_config for DAO operations like Where/Data.
|
|
||||||
type ImTenantConfig struct {
|
|
||||||
g.Meta `orm:"table:im_tenant_config, do:true"`
|
|
||||||
Id any //
|
|
||||||
TenantId any //
|
|
||||||
MaxUsers any // 0 means unlimited
|
|
||||||
MaxGroups any // 0 means unlimited
|
|
||||||
MaxGroupMembers any // 0 means unlimited
|
|
||||||
DefaultPolicyGroupId any // Default user policy group
|
|
||||||
SecurityPolicyVersion any // Published security policy version
|
|
||||||
GlobalFriendEnabled any // Published global friend switch
|
|
||||||
MessageCurfewEnabled any // Published message curfew switch
|
|
||||||
CreatedAt *gtime.Time //
|
|
||||||
CreatedBy any //
|
|
||||||
UpdatedAt *gtime.Time //
|
|
||||||
UpdatedBy any //
|
|
||||||
}
|
|
||||||
@@ -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:""`
|
|
||||||
}
|
|
||||||
@@ -14,8 +14,17 @@ type ImTenant struct {
|
|||||||
Code string `json:"code" orm:"code" description:""`
|
Code string `json:"code" orm:"code" description:""`
|
||||||
Name string `json:"name" orm:"name" description:""`
|
Name string `json:"name" orm:"name" description:""`
|
||||||
TenantDesc string `json:"tenantDesc" orm:"tenant_desc" description:""`
|
TenantDesc string `json:"tenantDesc" orm:"tenant_desc" description:""`
|
||||||
DbName string `json:"dbName" orm:"db_name" description:""`
|
MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0表示ä¸é™åˆ¶"`
|
||||||
DbDsn string `json:"dbDsn" orm:"db_dsn" description:""`
|
MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0表示ä¸é™åˆ¶"`
|
||||||
|
MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0表示ä¸é™åˆ¶"`
|
||||||
|
RealnameReviewPolicy int `json:"realnameReviewPolicy" orm:"realname_review_policy" description:"实åå®¡æ ¸ç–略:0自动通过,1è½¬äººå·¥å®¡æ ¸"`
|
||||||
|
AliyunModerationEnabled int `json:"aliyunModerationEnabled" orm:"aliyun_moderation_enabled" description:"是å¦å¯ç”¨é˜¿é‡Œäº‘å†…å®¹å®¡æ ¸"`
|
||||||
|
LocalModerationEnabled int `json:"localModerationEnabled" orm:"local_moderation_enabled" description:"是å¦å¯ç”¨æœ¬åœ°å†…å®¹å®¡æ ¸"`
|
||||||
|
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" orm:"message_encryption_enabled" description:"是å¦å¯ç”¨æ¶ˆæ¯åР坆"`
|
||||||
|
DefaultPolicyGroupId int64 `json:"defaultPolicyGroupId" orm:"default_policy_group_id" description:"默认用户ç–略组"`
|
||||||
|
SecurityPolicyVersion uint `json:"securityPolicyVersion" orm:"security_policy_version" description:"å·²å‘布安全ç–略版本"`
|
||||||
|
GlobalFriendEnabled int `json:"globalFriendEnabled" orm:"global_friend_enabled" description:"å…¨å±€åŠ å¥½å‹å¼€å…³"`
|
||||||
|
MessageCurfewEnabled int `json:"messageCurfewEnabled" orm:"message_curfew_enabled" description:"消æ¯å®µç¦å¼€å…³"`
|
||||||
State int `json:"state" orm:"state" description:""`
|
State int `json:"state" orm:"state" description:""`
|
||||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
|
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
|
||||||
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
|
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
// =================================================================================
|
|
||||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
|
||||||
// =================================================================================
|
|
||||||
|
|
||||||
package entity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ImTenantConfig is the golang structure for table im_tenant_config.
|
|
||||||
type ImTenantConfig struct {
|
|
||||||
Id int64 `json:"id" orm:"id" description:""`
|
|
||||||
TenantId int64 `json:"tenantId" orm:"tenant_id" description:""`
|
|
||||||
MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"`
|
|
||||||
MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"`
|
|
||||||
MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"`
|
|
||||||
DefaultPolicyGroupId int64 `json:"defaultPolicyGroupId" orm:"default_policy_group_id" description:"Default user policy group"`
|
|
||||||
SecurityPolicyVersion uint `json:"securityPolicyVersion" orm:"security_policy_version" description:"Published security policy version"`
|
|
||||||
GlobalFriendEnabled int `json:"globalFriendEnabled" orm:"global_friend_enabled" description:"Published global friend switch"`
|
|
||||||
MessageCurfewEnabled int `json:"messageCurfewEnabled" orm:"message_curfew_enabled" description:"Published message curfew switch"`
|
|
||||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
|
|
||||||
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
|
|
||||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""`
|
|
||||||
UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""`
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ type CultivateStageRuleEditInp struct {
|
|||||||
RequiredPoints uint `json:"requiredPoints"`
|
RequiredPoints uint `json:"requiredPoints"`
|
||||||
WaterPoints uint `json:"waterPoints"`
|
WaterPoints uint `json:"waterPoints"`
|
||||||
FertilizePoints uint `json:"fertilizePoints"`
|
FertilizePoints uint `json:"fertilizePoints"`
|
||||||
|
WaitHours uint `json:"waitHours"`
|
||||||
AllowedActions []string `json:"allowedActions"`
|
AllowedActions []string `json:"allowedActions"`
|
||||||
ImageFileId string `json:"imageFileId"`
|
ImageFileId string `json:"imageFileId"`
|
||||||
AnimationFileId string `json:"animationFileId"`
|
AnimationFileId string `json:"animationFileId"`
|
||||||
@@ -64,8 +65,8 @@ func (in *CultivateFlowerEditInp) Filter(ctx context.Context) error {
|
|||||||
return gerror.New("成长阶段不能重复")
|
return gerror.New("成长阶段不能重复")
|
||||||
}
|
}
|
||||||
seen[rule.Stage] = true
|
seen[rule.Stage] = true
|
||||||
if rule.Stage != "bloom" && rule.RequiredPoints == 0 {
|
if rule.Stage != "bloom" && rule.RequiredPoints == 0 && rule.WaitHours == 0 {
|
||||||
return gerror.New("非开花阶段升级阈值必须大于0")
|
return gerror.New("非开花阶段必须配置升级阈值或等待时长")
|
||||||
}
|
}
|
||||||
if rule.NextStage != "" && !validStage[rule.NextStage] {
|
if rule.NextStage != "" && !validStage[rule.NextStage] {
|
||||||
return gerror.New("下一阶段不正确")
|
return gerror.New("下一阶段不正确")
|
||||||
|
|||||||
@@ -16,13 +16,18 @@ import (
|
|||||||
|
|
||||||
type ImAppReleaseListInp struct {
|
type ImAppReleaseListInp struct {
|
||||||
form.PageReq
|
form.PageReq
|
||||||
|
AppCode string `json:"appCode" dc:"应用标识"`
|
||||||
Platform string `json:"platform" dc:"平台"`
|
Platform string `json:"platform" dc:"平台"`
|
||||||
Channel string `json:"channel" dc:"渠道"`
|
Channel string `json:"channel" dc:"渠道"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImAppReleaseListInp) Filter(ctx context.Context) (err error) {
|
func (in *ImAppReleaseListInp) Filter(ctx context.Context) (err error) {
|
||||||
|
in.AppCode = strings.TrimSpace(in.AppCode)
|
||||||
in.Platform = strings.TrimSpace(in.Platform)
|
in.Platform = strings.TrimSpace(in.Platform)
|
||||||
in.Channel = strings.TrimSpace(in.Channel)
|
in.Channel = strings.TrimSpace(in.Channel)
|
||||||
|
if in.AppCode != "" && !dict.HasOptionKey(consts.ImAppCodeOptions, in.AppCode) {
|
||||||
|
return gerror.New("应用标识不合法")
|
||||||
|
}
|
||||||
if in.Platform != "" && !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
|
if in.Platform != "" && !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
|
||||||
return gerror.New("客户端平台不合法")
|
return gerror.New("客户端平台不合法")
|
||||||
}
|
}
|
||||||
@@ -31,6 +36,7 @@ func (in *ImAppReleaseListInp) Filter(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
type ImAppReleaseModel struct {
|
type ImAppReleaseModel struct {
|
||||||
Id int64 `json:"id" dc:"ID"`
|
Id int64 `json:"id" dc:"ID"`
|
||||||
|
AppCode string `json:"appCode" dc:"应用标识"`
|
||||||
Platform string `json:"platform" dc:"平台"`
|
Platform string `json:"platform" dc:"平台"`
|
||||||
Channel string `json:"channel" dc:"渠道"`
|
Channel string `json:"channel" dc:"渠道"`
|
||||||
LatestVersion string `json:"latestVersion" dc:"最新版本"`
|
LatestVersion string `json:"latestVersion" dc:"最新版本"`
|
||||||
@@ -48,6 +54,7 @@ type ImAppReleaseModel struct {
|
|||||||
|
|
||||||
type ImAppReleaseEditInp struct {
|
type ImAppReleaseEditInp struct {
|
||||||
Id int64 `json:"id" dc:"ID"`
|
Id int64 `json:"id" dc:"ID"`
|
||||||
|
AppCode string `json:"appCode" dc:"应用标识"`
|
||||||
Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"`
|
Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"`
|
||||||
Channel string `json:"channel" v:"required#渠道不能为空" dc:"渠道"`
|
Channel string `json:"channel" v:"required#渠道不能为空" dc:"渠道"`
|
||||||
LatestVersion string `json:"latestVersion" v:"required#最新版本不能为空" dc:"最新版本"`
|
LatestVersion string `json:"latestVersion" v:"required#最新版本不能为空" dc:"最新版本"`
|
||||||
@@ -61,6 +68,7 @@ type ImAppReleaseEditInp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImAppReleaseEditInp) Filter(ctx context.Context) (err error) {
|
func (in *ImAppReleaseEditInp) Filter(ctx context.Context) (err error) {
|
||||||
|
in.AppCode = strings.TrimSpace(in.AppCode)
|
||||||
in.Platform = strings.TrimSpace(in.Platform)
|
in.Platform = strings.TrimSpace(in.Platform)
|
||||||
in.Channel = strings.TrimSpace(in.Channel)
|
in.Channel = strings.TrimSpace(in.Channel)
|
||||||
in.LatestVersion = strings.TrimSpace(in.LatestVersion)
|
in.LatestVersion = strings.TrimSpace(in.LatestVersion)
|
||||||
@@ -69,6 +77,12 @@ func (in *ImAppReleaseEditInp) Filter(ctx context.Context) (err error) {
|
|||||||
in.StoreUrl = strings.TrimSpace(in.StoreUrl)
|
in.StoreUrl = strings.TrimSpace(in.StoreUrl)
|
||||||
in.PackageUrl = strings.TrimSpace(in.PackageUrl)
|
in.PackageUrl = strings.TrimSpace(in.PackageUrl)
|
||||||
in.Checksum = strings.TrimSpace(in.Checksum)
|
in.Checksum = strings.TrimSpace(in.Checksum)
|
||||||
|
if in.AppCode == "" {
|
||||||
|
in.AppCode = consts.ImAppCodeIm
|
||||||
|
}
|
||||||
|
if !dict.HasOptionKey(consts.ImAppCodeOptions, in.AppCode) {
|
||||||
|
return gerror.New("应用标识不合法")
|
||||||
|
}
|
||||||
if !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
|
if !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
|
||||||
return gerror.New("客户端平台不合法")
|
return gerror.New("客户端平台不合法")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ type ImLoginFailureLogModel struct {
|
|||||||
UserAgent string `json:"userAgent" dc:"客户端标识"`
|
UserAgent string `json:"userAgent" dc:"客户端标识"`
|
||||||
RiskLevel string `json:"riskLevel" dc:"风险等级"`
|
RiskLevel string `json:"riskLevel" dc:"风险等级"`
|
||||||
RiskAction string `json:"riskAction" dc:"风控动作"`
|
RiskAction string `json:"riskAction" dc:"风控动作"`
|
||||||
|
LockedNow bool `json:"lockedNow" dc:"当前是否仍处于锁定中(实时状态,非历史快照)"`
|
||||||
RequestId string `json:"requestId" dc:"请求ID"`
|
RequestId string `json:"requestId" dc:"请求ID"`
|
||||||
AttemptAt *gtime.Time `json:"attemptAt" dc:"尝试时间"`
|
AttemptAt *gtime.Time `json:"attemptAt" dc:"尝试时间"`
|
||||||
Metadata string `json:"metadata" dc:"元数据"`
|
Metadata string `json:"metadata" dc:"元数据"`
|
||||||
|
|||||||
@@ -78,16 +78,6 @@ func TestImSecurityPolicySaveFilterForcesEveryFriendEntryOff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImSecurityPolicyPublishFilter(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: " "}).Filter(ctx); err == nil {
|
|
||||||
t.Fatal("空发布原因应被拒绝")
|
|
||||||
}
|
|
||||||
if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: "管理员发布"}).Filter(ctx); err != nil {
|
|
||||||
t.Fatalf("合法发布参数不应报错: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImSecurityIPRuleEditFilter(t *testing.T) {
|
func TestImSecurityIPRuleEditFilter(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
valid := &ImSecurityIPRuleEditInp{
|
valid := &ImSecurityIPRuleEditInp{
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Package sysin
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sysin
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/os/gtime"
|
||||||
|
|
||||||
|
// ImPublicUploadModel 企业注册入口公共上传文件
|
||||||
|
type ImPublicUploadModel struct {
|
||||||
|
Id int64 `json:"id" dc:"ID"`
|
||||||
|
FileId string `json:"fileId" dc:"文件ID"`
|
||||||
|
Purpose string `json:"purpose" dc:"用途"`
|
||||||
|
MimeType string `json:"mimeType" dc:"MIME类型"`
|
||||||
|
Size int64 `json:"size" dc:"文件大小"`
|
||||||
|
Etag string `json:"etag" dc:"ETag"`
|
||||||
|
Location string `json:"location" dc:"存储位置"`
|
||||||
|
Url string `json:"url" dc:"访问地址"`
|
||||||
|
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
UploadedAt *gtime.Time `json:"uploadedAt" dc:"上传完成时间"`
|
||||||
|
}
|
||||||
@@ -255,25 +255,6 @@ func normalizeLoginRateLimit(value *gjson.Json) (*gjson.Json, error) {
|
|||||||
return gjson.New(map[string]any{"enabled": true, "password_max_attempts": maxAttempts, "lock_seconds": lockSeconds, "window_seconds": windowSeconds}), nil
|
return gjson.New(map[string]any{"enabled": true, "password_max_attempts": maxAttempts, "lock_seconds": lockSeconds, "window_seconds": windowSeconds}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImSecurityPolicyPublishInp struct {
|
|
||||||
Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"`
|
|
||||||
Reason string `json:"reason" v:"required#发布原因不能为空"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (in *ImSecurityPolicyPublishInp) Filter(ctx context.Context) error {
|
|
||||||
in.Reason = strings.TrimSpace(in.Reason)
|
|
||||||
if in.Id <= 0 {
|
|
||||||
return gerror.New("策略ID不合法")
|
|
||||||
}
|
|
||||||
if in.Reason == "" {
|
|
||||||
return gerror.New("发布原因不能为空")
|
|
||||||
}
|
|
||||||
if len([]rune(in.Reason)) > 500 {
|
|
||||||
return gerror.New("发布原因不能超过500个字符")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImSecurityPolicyRestoreInp struct {
|
type ImSecurityPolicyRestoreInp struct {
|
||||||
TenantId int64 `json:"tenantId"`
|
TenantId int64 `json:"tenantId"`
|
||||||
Reason string `json:"reason" v:"required#恢复原因不能为空"`
|
Reason string `json:"reason" v:"required#恢复原因不能为空"`
|
||||||
|
|||||||
@@ -20,51 +20,25 @@ import (
|
|||||||
|
|
||||||
// ImTenantInsertFields 新增IM租户字段过滤
|
// ImTenantInsertFields 新增IM租户字段过滤
|
||||||
type ImTenantInsertFields struct {
|
type ImTenantInsertFields struct {
|
||||||
Code string `json:"code" dc:"租户编码"`
|
Id int64 `json:"id" dc:"商户ID"`
|
||||||
Name string `json:"name" dc:"租户名称"`
|
Code string `json:"code" dc:"商户编码"`
|
||||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
Name string `json:"name" dc:"商户名称"`
|
||||||
DbName string `json:"dbName" dc:"业务库名"`
|
TenantDesc string `json:"tenantDesc" dc:"商户描述"`
|
||||||
DbDsn string `json:"dbDsn" dc:"独立连接串"`
|
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||||
|
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||||
|
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||||
|
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
||||||
|
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||||
|
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||||
|
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||||
State int `json:"state" dc:"状态"`
|
State int `json:"state" dc:"状态"`
|
||||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImTenantConfigUpdateFields 修改IM租户配置字段过滤
|
|
||||||
type ImTenantConfigUpdateFields struct {
|
|
||||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
|
||||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
|
||||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
|
||||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
|
||||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
|
||||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
|
||||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
|
||||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImTenantConfigInsertFields 新增IM租户配置字段过滤
|
|
||||||
type ImTenantConfigInsertFields struct {
|
|
||||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
|
||||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
|
||||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
|
||||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
|
||||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
|
||||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
|
||||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
|
||||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
|
||||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImTenantEditInp 修改/新增IM租户
|
// ImTenantEditInp 修改/新增IM租户
|
||||||
type ImTenantEditInp struct {
|
type ImTenantEditInp struct {
|
||||||
entity.ImTenant
|
entity.ImTenant
|
||||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
|
||||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
|
||||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
|
||||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
|
||||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
|
||||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
|
||||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
|
||||||
AdminUsername string `json:"adminUsername" dc:"新增商户时自动创建的后台管理员账号名"`
|
AdminUsername string `json:"adminUsername" dc:"新增商户时自动创建的后台管理员账号名"`
|
||||||
AdminPassword string `json:"adminPassword" dc:"新增商户时自动创建的后台管理员初始密码"`
|
AdminPassword string `json:"adminPassword" dc:"新增商户时自动创建的后台管理员初始密码"`
|
||||||
}
|
}
|
||||||
@@ -119,27 +93,17 @@ func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) {
|
|||||||
type ImTenantViewModel struct {
|
type ImTenantViewModel struct {
|
||||||
entity.ImTenant
|
entity.ImTenant
|
||||||
AdminUsername string `json:"adminUsername" dc:"商户管理员账号"`
|
AdminUsername string `json:"adminUsername" dc:"商户管理员账号"`
|
||||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
|
||||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
|
||||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
|
||||||
AliyunModerationEnabled int `json:"aliyunModerationEnabled" dc:"是否启用阿里云内容审核"`
|
|
||||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
|
||||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
|
||||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
|
||||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
|
||||||
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
|
|
||||||
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImTenantListInp 获取IM租户列表
|
// ImTenantListInp 获取IM租户列表
|
||||||
type ImTenantListInp struct {
|
type ImTenantListInp struct {
|
||||||
form.PageReq
|
form.PageReq
|
||||||
|
form.Sorters
|
||||||
Id int64 `json:"id" dc:"ID"`
|
Id int64 `json:"id" dc:"ID"`
|
||||||
Code string `json:"code" dc:"租户编码"`
|
Code string `json:"code" dc:"租户编码"`
|
||||||
Name string `json:"name" dc:"租户名称"`
|
Name string `json:"name" dc:"租户名称"`
|
||||||
DbName string `json:"dbName" dc:"业务库名"`
|
|
||||||
State *int `json:"state" dc:"状态"`
|
State *int `json:"state" dc:"状态"`
|
||||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
}
|
}
|
||||||
@@ -153,7 +117,6 @@ type ImTenantListModel struct {
|
|||||||
Code string `json:"code" dc:"租户编码"`
|
Code string `json:"code" dc:"租户编码"`
|
||||||
Name string `json:"name" dc:"租户名称"`
|
Name string `json:"name" dc:"租户名称"`
|
||||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||||
DbName string `json:"dbName" dc:"业务库名"`
|
|
||||||
State int `json:"state" dc:"状态"`
|
State int `json:"state" dc:"状态"`
|
||||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||||
@@ -162,7 +125,6 @@ type ImTenantListModel struct {
|
|||||||
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"`
|
||||||
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"`
|
||||||
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"`
|
||||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
|
||||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||||
@@ -204,8 +166,6 @@ type ImTenantUsageModel struct {
|
|||||||
Id int64 `json:"id" dc:"租户ID"`
|
Id int64 `json:"id" dc:"租户ID"`
|
||||||
Code string `json:"code" dc:"租户编码"`
|
Code string `json:"code" dc:"租户编码"`
|
||||||
Name string `json:"name" dc:"租户名称"`
|
Name string `json:"name" dc:"租户名称"`
|
||||||
DbName string `json:"dbName" dc:"业务库名"`
|
|
||||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
|
||||||
DbReachable bool `json:"dbReachable" dc:"业务库是否可达"`
|
DbReachable bool `json:"dbReachable" dc:"业务库是否可达"`
|
||||||
DbCheckError string `json:"dbCheckError" dc:"业务库检查错误"`
|
DbCheckError string `json:"dbCheckError" dc:"业务库检查错误"`
|
||||||
SchemaVersion string `json:"schemaVersion" dc:"Schema版本"`
|
SchemaVersion string `json:"schemaVersion" dc:"Schema版本"`
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Package sysin
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sysin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImTenantApplicationListInp 获取企业注册申请列表
|
||||||
|
type ImTenantApplicationListInp struct {
|
||||||
|
form.PageReq
|
||||||
|
ApplicationId string `json:"applicationId" dc:"申请ID"`
|
||||||
|
CreditCode string `json:"creditCode" dc:"统一社会信用代码"`
|
||||||
|
EnterpriseEmail string `json:"enterpriseEmail" dc:"企业邮箱"`
|
||||||
|
TenantCodeHint string `json:"tenantCodeHint" dc:"商户编码线索"`
|
||||||
|
Status string `json:"status" dc:"状态"`
|
||||||
|
Keyword string `json:"keyword" dc:"关键词"`
|
||||||
|
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *ImTenantApplicationListInp) Filter(ctx context.Context) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImTenantApplicationListModel struct {
|
||||||
|
Id int64 `json:"id" dc:"ID"`
|
||||||
|
ApplicationId string `json:"applicationId" dc:"申请ID"`
|
||||||
|
CreditCode string `json:"creditCode" dc:"统一社会信用代码"`
|
||||||
|
EnterpriseEmail string `json:"enterpriseEmail" dc:"企业邮箱"`
|
||||||
|
LicenseFileId string `json:"licenseFileId" dc:"营业执照文件ID"`
|
||||||
|
TenantCodeHint string `json:"tenantCodeHint" dc:"商户编码线索"`
|
||||||
|
Status string `json:"status" dc:"状态"`
|
||||||
|
EstimatedReviewHour int `json:"estimatedReviewHour" dc:"预计审核小时"`
|
||||||
|
SubmittedAt *gtime.Time `json:"submittedAt" dc:"提交时间"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImTenantApplicationViewInp 获取企业注册申请详情
|
||||||
|
type ImTenantApplicationViewInp struct {
|
||||||
|
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *ImTenantApplicationViewInp) Filter(ctx context.Context) (err error) {
|
||||||
|
if in.Id <= 0 {
|
||||||
|
return gerror.New("ID不能为空")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImTenantApplicationViewModel struct {
|
||||||
|
ImTenantApplicationListModel
|
||||||
|
ClientContext string `json:"clientContext" dc:"客户端上下文"`
|
||||||
|
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||||
|
LicenseFile *ImPublicUploadModel `json:"licenseFile" dc:"营业执照文件"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Package sysin
|
||||||
|
// @Link https://github.com/bufanyun/hotgo
|
||||||
|
// @Copyright Copyright (c) 2026 HotGo
|
||||||
|
// @Author Ms <133814250@qq.com>
|
||||||
|
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||||
|
package sysin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImTenantEntryFeedbackListInp 获取企业入口反馈列表
|
||||||
|
type ImTenantEntryFeedbackListInp struct {
|
||||||
|
form.PageReq
|
||||||
|
FeedbackId string `json:"feedbackId" dc:"反馈ID"`
|
||||||
|
Contact string `json:"contact" dc:"联系方式"`
|
||||||
|
TenantCodeHint string `json:"tenantCodeHint" dc:"商户编码线索"`
|
||||||
|
Status string `json:"status" dc:"状态"`
|
||||||
|
Keyword string `json:"keyword" dc:"关键词"`
|
||||||
|
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *ImTenantEntryFeedbackListInp) Filter(ctx context.Context) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImTenantEntryFeedbackListModel struct {
|
||||||
|
Id int64 `json:"id" dc:"ID"`
|
||||||
|
FeedbackId string `json:"feedbackId" dc:"反馈ID"`
|
||||||
|
Description string `json:"description" dc:"问题描述"`
|
||||||
|
Contact string `json:"contact" dc:"联系方式"`
|
||||||
|
TenantCodeHint string `json:"tenantCodeHint" dc:"商户编码线索"`
|
||||||
|
Status string `json:"status" dc:"状态"`
|
||||||
|
SubmittedAt *gtime.Time `json:"submittedAt" dc:"提交时间"`
|
||||||
|
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||||
|
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImTenantEntryFeedbackViewInp 获取企业入口反馈详情
|
||||||
|
type ImTenantEntryFeedbackViewInp struct {
|
||||||
|
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in *ImTenantEntryFeedbackViewInp) Filter(ctx context.Context) (err error) {
|
||||||
|
if in.Id <= 0 {
|
||||||
|
return gerror.New("ID不能为空")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImTenantEntryFeedbackViewModel struct {
|
||||||
|
ImTenantEntryFeedbackListModel
|
||||||
|
ScreenshotFileIds string `json:"screenshotFileIds" dc:"截图文件ID JSON"`
|
||||||
|
ClientContext string `json:"clientContext" dc:"客户端上下文"`
|
||||||
|
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||||
|
ScreenshotFiles []*ImPublicUploadModel `json:"screenshotFiles" dc:"截图文件"`
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user