Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0596599db9 | ||
|
|
40ac52923e | ||
|
|
c9673ca871 | ||
|
|
197c6c86f4 | ||
|
|
a2f328f97e | ||
|
|
c4ab970fb7 | ||
|
|
8fad32e396 | ||
|
|
6f331a07fc | ||
|
|
b4174a6412 | ||
|
|
0381984939 | ||
|
|
6348ace38e | ||
|
|
e8dd1eaa7d | ||
|
|
42cfe232fe | ||
|
|
4ae69706a5 | ||
|
|
3ef328a5a3 | ||
|
|
2b93d27a4e | ||
|
|
97990785cb | ||
|
|
b68c048383 | ||
|
|
f154b9e3c3 | ||
|
|
e2ca0607af | ||
|
|
6afeb65b91 | ||
|
|
e5ab79c40d | ||
|
|
8675fe333d | ||
|
|
fd4ff85191 | ||
|
|
6bf364ec0b | ||
|
|
47ed097923 | ||
|
|
e5cfc80916 | ||
|
|
c7265252d8 | ||
|
|
f6766d3dbd | ||
|
|
7c8b4d9b7c | ||
|
|
348cbcaf96 | ||
|
|
2a35cb4358 | ||
|
|
be6c5b525c | ||
|
|
9ed8aaf142 | ||
|
|
4ed54ca392 | ||
|
|
5173bc0aad | ||
|
|
fec2a43b9c | ||
|
|
32491e9cdd | ||
|
|
fe5c7af7e2 | ||
|
|
156e8b9c9f | ||
|
|
f8aaae42ff | ||
|
|
82379b8823 | ||
|
|
3e40554229 | ||
|
|
e159f9a88e | ||
|
|
c258e0473b | ||
|
|
5dbbf39cce | ||
|
|
50a2781b23 | ||
|
|
a6f7ac41b0 | ||
|
|
c1a6a41575 | ||
|
|
f44e8adb72 |
+16
-21
@@ -1,10 +1,10 @@
|
||||
name: Deploy im-manager (Blue-Green)
|
||||
name: Deploy im-manager (k3s test cluster)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: '镜像tag,留空则使用commit短哈希'
|
||||
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)'
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
@@ -17,26 +17,21 @@ jobs:
|
||||
id: vars
|
||||
run: |
|
||||
TAG="${{ github.event.inputs.image_tag }}"
|
||||
[ -z "$TAG" ] && TAG=$(git rev-parse --short HEAD)
|
||||
[ -z "$TAG" ] && TAG=test
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 构建镜像(前端+后端一体,自包含多阶段构建)
|
||||
run: docker build -t im-manager:${{ steps.vars.outputs.tag }} .
|
||||
- name: 登录镜像仓库
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin
|
||||
|
||||
- name: 蓝绿部署
|
||||
env:
|
||||
MYSQL_DSN: ${{ secrets.MYSQL_DSN }}
|
||||
REDIS_ADDR: ${{ secrets.REDIS_ADDR }}
|
||||
REDIS_DB: ${{ secrets.REDIS_DB }}
|
||||
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
|
||||
TOKEN_SECRET_KEY: ${{ secrets.TOKEN_SECRET_KEY }}
|
||||
IM_ADMIN_API_BASE_URL: ${{ secrets.IM_ADMIN_API_BASE_URL }}
|
||||
IM_ADMIN_API_SERVICE_ID: ${{ secrets.IM_ADMIN_API_SERVICE_ID }}
|
||||
IM_ADMIN_API_SECRET: ${{ secrets.IM_ADMIN_API_SECRET }}
|
||||
IM_INTERNAL_API_BASE_URL: ${{ secrets.IM_INTERNAL_API_BASE_URL }}
|
||||
IM_INTERNAL_API_SERVICE_NAME: ${{ secrets.IM_INTERNAL_API_SERVICE_NAME }}
|
||||
IM_INTERNAL_API_SECRET: ${{ secrets.IM_INTERNAL_API_SECRET }}
|
||||
IM_CONFIG_API_SERVICE_NAME: ${{ secrets.IM_CONFIG_API_SERVICE_NAME }}
|
||||
IM_CONFIG_API_SECRET: ${{ secrets.IM_CONFIG_API_SECRET }}
|
||||
- name: 构建并推送镜像(前端+后端一体,自包含多阶段构建)
|
||||
run: |
|
||||
IMAGE=im-manager:${{ steps.vars.outputs.tag }} /opt/apps/im-manager/deploy.sh
|
||||
docker build -t git.118im.com/im_team/im-manager:${{ steps.vars.outputs.tag }} .
|
||||
docker push git.118im.com/im_team/im-manager:${{ steps.vars.outputs.tag }}
|
||||
|
||||
- name: 部署到k3s测试集群
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
kubectl rollout restart deployment/im-manager -n manager-system
|
||||
kubectl rollout status deployment/im-manager -n manager-system --timeout=120s
|
||||
|
||||
@@ -163,3 +163,6 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Trae local debugging artifacts
|
||||
.trae/
|
||||
|
||||
|
||||
+27
-14
@@ -21,11 +21,16 @@ COPY server/ .
|
||||
RUN rm -rf resource/public/admin && mkdir -p resource/public/admin
|
||||
COPY --from=web-build /web/dist/ ./resource/public/admin/
|
||||
|
||||
# main.go does `import _ "hotgo/internal/packed"`, a file normally generated by
|
||||
# `gf build` (see server/hack/config.yaml: packSrc/packDst) and gitignored —
|
||||
# generate it explicitly with the same gf CLI version pinned in go.mod.
|
||||
RUN go install github.com/gogf/gf/cmd/gf/v2@v2.10.0
|
||||
RUN gf pack resource internal/packed/packed.go
|
||||
# main.go imports internal/packed. Generate the packed resource file directly
|
||||
# through GoFrame's library API instead of installing the full gf CLI in CI.
|
||||
RUN printf '%s\n' \
|
||||
'package main' \
|
||||
'import "github.com/gogf/gf/v2/os/gres"' \
|
||||
'func main() {' \
|
||||
' if err := gres.PackToGoFileWithOption("resource", "internal/packed/packed.go", "packed", gres.Option{}); err != nil { panic(err) }' \
|
||||
'}' > /tmp/pack.go \
|
||||
&& go run /tmp/pack.go \
|
||||
&& rm /tmp/pack.go
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o /out/hotgo main.go
|
||||
|
||||
@@ -37,19 +42,27 @@ RUN apk add --no-cache ca-certificates gettext bash tzdata
|
||||
WORKDIR /app
|
||||
|
||||
ENV QUEUE_DRIVER=kafka
|
||||
ENV KAFKA_ADDRESS=64.118.137.182:9094
|
||||
ENV KAFKA_ADDRESS=kafka.118im.com:30835
|
||||
ENV KAFKA_VERSION=2.4.0
|
||||
ENV KAFKA_USERNAME=kafka-external
|
||||
ENV KAFKA_PASSWORD=gr58tNnlZOVGW3AmMbL2
|
||||
ENV KAFKA_SASL_MECHANISM=SCRAM-SHA-512
|
||||
ENV IM_ADMIN_API_BASE_URL=http://im-server:6060
|
||||
ENV KAFKA_USERNAME=
|
||||
ENV KAFKA_PASSWORD=
|
||||
ENV KAFKA_SASL_MECHANISM=
|
||||
ENV MYSQL_DSN=
|
||||
ENV REDIS_ADDR=redis.118im.com:30637
|
||||
ENV REDIS_DB=2
|
||||
ENV REDIS_PASSWORD=
|
||||
ENV TOKEN_SECRET_KEY=hotgo123
|
||||
ENV IM_ADMIN_API_BASE_URL=https://api.118im.com
|
||||
ENV IM_ADMIN_API_SERVICE_ID=hotgo-admin
|
||||
ENV IM_ADMIN_API_SECRET=8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b
|
||||
ENV IM_INTERNAL_API_BASE_URL=http://im-server:6060
|
||||
ENV IM_ADMIN_API_SECRET=
|
||||
ENV IM_INTERNAL_API_BASE_URL=https://api.118im.com
|
||||
ENV IM_INTERNAL_API_SERVICE_NAME=hotgo-admin
|
||||
ENV IM_INTERNAL_API_SECRET=8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b
|
||||
ENV IM_INTERNAL_API_SECRET=
|
||||
ENV IM_WORKBENCH_API_BASE_URL=http://im-workbench.workbench-system.svc.cluster.local:6061
|
||||
ENV IM_WORKBENCH_API_SERVICE_NAME=hotgo-admin
|
||||
ENV IM_WORKBENCH_API_SECRET=hotgo-workbench-internal
|
||||
ENV IM_CONFIG_API_SERVICE_NAME=im-server
|
||||
ENV IM_CONFIG_API_SECRET=b96832c04dbfb6dd768930ec9f082ecd90d87a1ffdaa493aa070617c48de9097
|
||||
ENV IM_CONFIG_API_SECRET=
|
||||
|
||||
COPY --from=server-build /src/manifest ./manifest
|
||||
COPY --from=server-build /src/hack ./hack
|
||||
|
||||
@@ -947,6 +947,22 @@ CREATE TABLE IF NOT EXISTS `im_login_failure_logs` (
|
||||
KEY `idx_im_login_failure_request` (`request_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_login_lockouts` (
|
||||
`id` bigint NOT NULL,
|
||||
`user_id` bigint DEFAULT NULL,
|
||||
`account_hash` char(64) NOT NULL,
|
||||
`failed_attempts` int NOT NULL DEFAULT '0',
|
||||
`window_started_at` datetime(3) DEFAULT NULL,
|
||||
`locked_until` datetime(3) DEFAULT NULL,
|
||||
`last_attempt_at` datetime(3) DEFAULT NULL,
|
||||
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_im_login_lockouts_account` (`account_hash`),
|
||||
KEY `idx_im_login_lockouts_user` (`user_id`),
|
||||
KEY `idx_im_login_lockouts_locked` (`locked_until`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_mailbox_config` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`enabled` tinyint NOT NULL DEFAULT '1',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
-- 商户级内容审核开关。
|
||||
-- 可重复执行;新商户默认关闭,现有 default 商户保持当前审核行为。
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
|
||||
AND COLUMN_NAME = 'aliyun_moderation_enabled') = 0,
|
||||
'ALTER TABLE `im_tenant_config` ADD COLUMN `aliyun_moderation_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用阿里云内容审核'' AFTER `max_group_members`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
|
||||
AND COLUMN_NAME = 'local_moderation_enabled') = 0,
|
||||
'ALTER TABLE `im_tenant_config` ADD COLUMN `local_moderation_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用本地内容审核'' AFTER `aliyun_moderation_enabled`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
UPDATE `im_tenant_config` c
|
||||
JOIN `im_tenant` t ON t.`id` = c.`tenant_id`
|
||||
SET c.`aliyun_moderation_enabled` = 1,
|
||||
c.`local_moderation_enabled` = 1
|
||||
WHERE t.`code` = 'default';
|
||||
@@ -0,0 +1,137 @@
|
||||
-- 消息加密密钥管理。
|
||||
-- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET @now := NOW();
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
|
||||
AND COLUMN_NAME = 'message_encryption_enabled') = 0,
|
||||
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` bigint NOT NULL COMMENT '商户ID',
|
||||
`key_version` int unsigned NOT NULL COMMENT '密钥版本',
|
||||
`algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法',
|
||||
`encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK',
|
||||
`dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce',
|
||||
`status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled',
|
||||
`activated_at` datetime DEFAULT NULL,
|
||||
`retired_at` datetime DEFAULT NULL,
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`updated_by` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`),
|
||||
KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本';
|
||||
|
||||
INSERT INTO `hg_sys_dict_type`
|
||||
(`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999,
|
||||
'仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret'
|
||||
);
|
||||
|
||||
INSERT INTO `hg_sys_dict_data`
|
||||
(`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret',
|
||||
'', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `hg_sys_dict_data`
|
||||
WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01'
|
||||
);
|
||||
|
||||
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
|
||||
SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1);
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index',
|
||||
'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index',
|
||||
1, 'imManager', 0, 0, '', 0, 0, 0, 3,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80,
|
||||
'商户消息加密开关及密钥版本管理', 1, @now, @now
|
||||
WHERE @settingsId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex');
|
||||
|
||||
SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1);
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `pid` = @settingsId,
|
||||
`title` = '消息密钥管理',
|
||||
`path` = 'message-encryption/index',
|
||||
`icon` = 'KeyOutlined',
|
||||
`permissions` = '/imMessageEncryption/list',
|
||||
`component` = '/imMessageEncryption/index',
|
||||
`active_menu` = 'imManager',
|
||||
`level` = 3,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '),
|
||||
`sort` = 80,
|
||||
`hidden` = 0,
|
||||
`status` = 1,
|
||||
`updated_at` = @now
|
||||
WHERE `id` = @keyMenuId;
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '',
|
||||
'/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '',
|
||||
'/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate');
|
||||
|
||||
INSERT INTO `hg_admin_menu`
|
||||
(`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`,
|
||||
`permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`,
|
||||
`frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`,
|
||||
`status`, `created_at`, `updated_at`)
|
||||
SELECT @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '',
|
||||
'/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4,
|
||||
CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now
|
||||
WHERE @keyMenuId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch');
|
||||
|
||||
UPDATE `hg_admin_menu`
|
||||
SET `level` = 4,
|
||||
`tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '),
|
||||
`updated_at` = @now
|
||||
WHERE `pid` = @keyMenuId AND `type` = 3;
|
||||
|
||||
INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, `id` FROM `hg_admin_menu`
|
||||
WHERE `name` IN (
|
||||
'imManager',
|
||||
'imSystemSettings',
|
||||
'imMessageEncryptionIndex',
|
||||
'imMessageEncryptionInitialize',
|
||||
'imMessageEncryptionRotate',
|
||||
'imMessageEncryptionSwitch'
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
-- 阿里云文本审核统一复用 hg_sys_config.im_third_party 中的公共凭证、地域和端点。
|
||||
-- 可重复执行;清理 im_external_config 中历史重复保存的密钥和连接参数。
|
||||
|
||||
INSERT INTO `im_external_config`
|
||||
(`tenant_id`, `service`, `provider`, `endpoint`, `enabled`, `status`, `config_json`, `secret_json`, `created_by`, `updated_by`)
|
||||
SELECT
|
||||
t.`id`,
|
||||
'moderation',
|
||||
'aliyun_green',
|
||||
'',
|
||||
1,
|
||||
'unconfigured',
|
||||
JSON_OBJECT(
|
||||
'nickname_service', 'nickname_detection_pro',
|
||||
'chat_service', 'chat_detection_pro',
|
||||
'timeout_ms', 1500,
|
||||
'failure_mode', 'allow'
|
||||
),
|
||||
JSON_OBJECT(),
|
||||
0,
|
||||
0
|
||||
FROM `im_tenant` t
|
||||
WHERE t.`code` = 'default'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`provider` = VALUES(`provider`),
|
||||
`endpoint` = VALUES(`endpoint`),
|
||||
`enabled` = VALUES(`enabled`),
|
||||
`status` = VALUES(`status`),
|
||||
`config_json` = VALUES(`config_json`),
|
||||
`secret_json` = VALUES(`secret_json`),
|
||||
`checked_at` = NULL,
|
||||
`check_result` = '',
|
||||
`updated_at` = CURRENT_TIMESTAMP;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
# IM 登录失败锁定 - 移动端对接文档
|
||||
|
||||
> 功能:用户登录密码连续输错达到配置阈值后,账号暂时锁定;锁定期间登录请求被服务端直接拒绝。本文档面向原生移动端(Android / iOS,基于 Tinode SDK)对接者。
|
||||
> 版本:v1(2026-09-20) | 服务端版本:im_server(已发布至 dev)
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能交互总览
|
||||
|
||||
```
|
||||
移动端登录(basic 认证,协议不变)
|
||||
├─ 密码正确 → 计数清零,登录成功(行为与现状完全一致)
|
||||
├─ 密码错误(未达阈值)→ 401 authentication failed(现状不变)
|
||||
├─ 密码错误达到阈值 → 429 账号锁定(message 含剩余时间)
|
||||
└─ 锁定期间再次登录(无论密码对错)→ 429,服务端不校验密码直接拒绝
|
||||
```
|
||||
|
||||
- **登录协议与请求结构零变更**:锁定逻辑全部在服务端(im_server basic 认证链路),移动端只做错误展示适配。
|
||||
- **阈值/时长由管理后台配置**:【安全管理→安全策略】的登录限频策略,按租户下发(不同租户配置可能不同),客户端不可配置、无需缓存。
|
||||
- **解锁途径**:① 锁定时间自然到期自动解锁;② 用户联系客服/管理员,在管理后台【日志审计→登录失败日志】手动解锁。两种途径均无需客户端参与。
|
||||
|
||||
## 2. 错误响应契约(核心对接点)
|
||||
|
||||
### 2.1 响应结构(WebSocket 传输)
|
||||
|
||||
登录 `{login}` 消息的 ctrl 响应:
|
||||
|
||||
| 场景 | code | text | params |
|
||||
|---|---|---|---|
|
||||
| 密码错误,未达阈值 | 401 | `authentication failed` | — |
|
||||
| 密码错误达到阈值,触发锁定 | **429** | `登录错误次数过多,账号已锁定,请 N 后再试`(N 按剩余时长显示:不足 1 分钟为「1 分钟以内」,不足 1 小时为「1 小时以内」,不足 1 天为「N 小时」,其他为「N 天」) | `{"what":"auth","retry_after_seconds":60}` |
|
||||
| 锁定期间再次登录 | **429** | 同上(N 随剩余时间递减) | 同上 |
|
||||
| 登录成功 | 200 | `ok` | 正常 auth 信息 |
|
||||
|
||||
实测样例(连错 2 次触发锁定,阈值为 2 的租户):
|
||||
|
||||
```json
|
||||
// 第 1 次错误密码
|
||||
{"ctrl": {"id": "t1", "code": 401, "text": "authentication failed", "ts": "..."}}
|
||||
|
||||
// 第 2 次错误密码(达到阈值)
|
||||
{"ctrl": {"id": "t1", "code": 429,
|
||||
"text": "登录错误次数过多,账号已锁定,请 1 分钟以内再试",
|
||||
"params": {"what": "auth", "retry_after_seconds": 60},
|
||||
"ts": "..."}}
|
||||
|
||||
// 锁定期间(约 12 秒后)正确密码登录,同样被拒绝且剩余时间递减
|
||||
{"ctrl": {"id": "t1", "code": 429,
|
||||
"text": "登录错误次数过多,账号已锁定,请 1 分钟以内再试",
|
||||
"params": {"what": "auth", "retry_after_seconds": 48},
|
||||
"ts": "..."}}
|
||||
```
|
||||
|
||||
### 2.2 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `code` | int | HTTP 语义状态码。**429 = 账号锁定**,与 401(密码错误)区分 |
|
||||
| `text` | string | **面向用户展示的完整中文文案,剩余时长自动格式化:不足 1 分钟显示「1 分钟以内」,不足 1 小时显示「1 小时以内」,不足 1 天按小时(向下取整),其他按天(向下取整)**。客户端直接展示即可,无需自行拼接时间 |
|
||||
| `params.retry_after_seconds` | int | 剩余锁定秒数(向上取整),可用于 UI 倒计时/禁用登录按钮 |
|
||||
| `params.what` | string | 固定 `auth`,表示错误发生在认证环节 |
|
||||
|
||||
### 2.3 gRPC 传输(Android SDK 默认)
|
||||
|
||||
gRPC 通道下服务端返回相同的 ctrl 结构(code/text/params 逐字段保留),SDK 会将其封装为带 code 的登录异常抛出。**识别方式不变:code == 429**。
|
||||
|
||||
## 3. 客户端处理建议
|
||||
|
||||
1. **错误识别**:登录失败回调中判断 `code == 429`(或 SDK 异常携带的 ctrl code)。仅 `429` 按锁定处理,`401`/`403` 维持现有文案与逻辑。
|
||||
2. **文案展示**:
|
||||
- 优先展示服务端 `text`(已含剩余分钟数,中文);
|
||||
- `text` 为空时的兜底文案:`登录错误次数过多,请稍后再试`。
|
||||
3. **禁止静默重试**:锁定期间重试不会带来额外后果(不刷新锁定时间、不计新失败),但每次都得到 429 提示,建议拿到 429 后**禁用登录按钮或展示倒计时**(用 `retry_after_seconds`),引导用户等待或走找回密码。
|
||||
4. **倒计时精度**:`retry_after_seconds` 为剩余秒数(比真实剩余时间多算 1 秒余量),客户端倒计时结束即可恢复登录,无需与服务端时钟同步。
|
||||
5. **多端一致**:Web 端(im_webapp)已按本契约实现(`errors.ts` 的 429 分支),移动端保持相同交互语义。
|
||||
|
||||
## 4. 服务端行为细节(影响测试预期)
|
||||
|
||||
| 行为 | 说明 |
|
||||
|---|---|
|
||||
| 计数对象 | 按**账号名**(username,不区分大小写)哈希计数,与设备/IP 无关:换设备登录同一账号同样累计 |
|
||||
| 计数窗口 | 滑动窗口 `window_seconds`(默认 30 分钟):窗口内累计达到阈值即锁定;窗口过期后重新计数 |
|
||||
| 锁定时长 | `lock_seconds`(默认 15 分钟),从触发锁定的那次失败请求起算 |
|
||||
| 锁定期内正确密码 | 仍返回 429(校验前置于密码比对),**且不刷新锁定时间**(剩余秒数持续递减) |
|
||||
| 锁定期内请求 | 不写入失败审计日志(避免日志膨胀) |
|
||||
| 成功登录 | 计数与锁定记录立即清零 |
|
||||
| 仅 basic 认证 | 锁定只作用于 `basic` scheme(账号密码)登录;token 重连不受影响 |
|
||||
| 策略关闭 | 租户未配置或 `enabled=false` 时,行为与旧版完全一致(无 429) |
|
||||
|
||||
## 5. 自测要点
|
||||
|
||||
| 用例 | 预期 |
|
||||
|---|---|
|
||||
| 错误密码 × (阈值-1) 次 | 每次 401,文案「账号或密码错误」 |
|
||||
| 错误密码第 N 次(达阈值) | 429 + 锁定文案 + `retry_after_seconds` ≈ lock_seconds |
|
||||
| 锁定期内用**正确**密码 | 仍 429,`retry_after_seconds` 递减 |
|
||||
| 等待锁定到期(或联系管理员解锁) | 正确密码登录成功 200 |
|
||||
| 登录成功后再连错 (阈值-1) 次 | 均 401(成功登录已清零历史计数) |
|
||||
|
||||
> 注:测试环境的阈值/时长以【安全管理→安全策略】当前发布版本为准,联调时可临时调小(如 2 次/60 秒)加速验证。
|
||||
@@ -0,0 +1,11 @@
|
||||
-- ----------------------------------------------------------------
|
||||
-- 登录失败锁定功能:登录失败日志页新增"解锁"按钮权限点
|
||||
-- 依赖:im_adfahi_batch2_menu.sql(imLoginFailureLogIndex 菜单已存在)
|
||||
-- ----------------------------------------------------------------
|
||||
SET @now := NOW();
|
||||
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
|
||||
SET @loginFailureId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imLoginFailureLogIndex' LIMIT 1);
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`)
|
||||
SELECT NULL,@loginFailureId,'解锁登录锁定','imLoginFailureLogUnlock','','','3','','/imLoginFailureLog/unlock','','','1','','0','0','','0','0','0','3',CONCAT('tr_',@imRootId,' tr_',@loginFailureId,' '),50,'','1',@now,@now
|
||||
WHERE @loginFailureId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imLoginFailureLogUnlock');
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ----------------------------------------------------------------
|
||||
-- 登录失败策略独立配置:安全策略菜单下新增"保存登录失败策略"按钮权限点
|
||||
-- 依赖:im_adfahi_policy_menu.sql(imSecurityPolicyIndex 菜单已存在)
|
||||
-- 幂等:重复执行不产生重复行
|
||||
-- ----------------------------------------------------------------
|
||||
USE hotgo;
|
||||
SET @now := NOW();
|
||||
SET @securityPolicyId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSecurityPolicyIndex' LIMIT 1);
|
||||
SET @policyPid := (SELECT `pid` FROM `hg_admin_menu` WHERE `name` = 'imSecurityPolicyIndex' LIMIT 1);
|
||||
SET @policyLevel := (SELECT `level` FROM `hg_admin_menu` WHERE `name` = 'imSecurityPolicyIndex' LIMIT 1);
|
||||
SET @policyTree := (SELECT `tree` FROM `hg_admin_menu` WHERE `name` = 'imSecurityPolicyIndex' LIMIT 1);
|
||||
|
||||
INSERT INTO `hg_admin_menu` (`id`,`pid`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`level`,`tree`,`sort`,`remark`,`status`,`created_at`,`updated_at`)
|
||||
SELECT NULL,@securityPolicyId,'保存登录失败策略','imSecurityPolicyLoginLockoutConfig','','','3','','/imSecurityPolicy/loginLockoutConfig','','','1','','0','0','','0','0','0',IFNULL(@policyLevel,2)+1,CONCAT(IFNULL(@policyTree,''),'tr_',@securityPolicyId,' '),80,'','1',@now,@now
|
||||
WHERE @securityPolicyId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imSecurityPolicyLoginLockoutConfig');
|
||||
@@ -0,0 +1,19 @@
|
||||
-- ----------------------------------------------------------------
|
||||
-- IM「联系我们」配置:字典管理新增 im_contact_us 字典类型及默认字典数据
|
||||
-- 用途:移动端通过 im-server 接口 /v1/im/contact-us 读取展示客服邮箱
|
||||
-- 依赖:无需前置 SQL
|
||||
-- 幂等:重复执行不产生重复行
|
||||
-- 说明:邮箱的最终值请在管理后台「系统管理 → 字典管理 → 联系我们」中维护
|
||||
-- ----------------------------------------------------------------
|
||||
USE hotgo;
|
||||
SET @now := NOW();
|
||||
|
||||
-- 字典类型:联系我们(挂载在系统枚举 sys 下)
|
||||
INSERT INTO `hg_sys_dict_type` (`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT (SELECT `id` FROM `hg_sys_dict_type` WHERE `type` = 'sys' LIMIT 1), '联系我们', 'im_contact_us', 0, '移动端联系我们配置', 1, @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_contact_us');
|
||||
|
||||
-- 字典数据:客服邮箱
|
||||
INSERT INTO `hg_sys_dict_data` (`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT '客服邮箱', 'support@example.com', 'string', 'im_contact_us', '', 1, 10, '移动端展示的客服邮箱', 1, @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_sys_dict_data` WHERE `type` = 'im_contact_us' AND `label` = '客服邮箱');
|
||||
@@ -0,0 +1,69 @@
|
||||
package cultivatecontent
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/cultivate/content/list" method:"get" tags:"养成管理" summary:"获取动态或评论审核列表"`
|
||||
sysin.CultivateContentListInp
|
||||
}
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.CultivateContentListModel `json:"list"`
|
||||
}
|
||||
type ReviewReq struct {
|
||||
g.Meta `path:"/cultivate/content/review" method:"post" tags:"养成管理" summary:"审核或下架动态评论"`
|
||||
sysin.CultivateContentReviewInp
|
||||
}
|
||||
type ReviewRes struct{}
|
||||
type BatchReviewReq struct {
|
||||
g.Meta `path:"/cultivate/content/batch-review" method:"post" tags:"养成管理" summary:"批量审核或下架动态评论"`
|
||||
sysin.CultivateContentBatchReviewInp
|
||||
}
|
||||
type BatchReviewRes struct {
|
||||
*sysin.CultivateContentBatchReviewModel
|
||||
}
|
||||
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/cultivate/content/view" method:"get" tags:"养成管理" summary:"获取内容审核详情"`
|
||||
sysin.CultivateContentViewInp
|
||||
}
|
||||
type ViewRes struct {
|
||||
*sysin.CultivateContentViewModel
|
||||
}
|
||||
type ReportListReq struct {
|
||||
g.Meta `path:"/cultivate/reports/list" method:"get" tags:"养成管理" summary:"获取举报列表"`
|
||||
sysin.CultivateReportListInp
|
||||
}
|
||||
type ReportListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.CultivateReportListModel `json:"list"`
|
||||
}
|
||||
type ReportHandleReq struct {
|
||||
g.Meta `path:"/cultivate/reports/handle" method:"post" tags:"养成管理" summary:"处理举报"`
|
||||
sysin.CultivateReportHandleInp
|
||||
}
|
||||
type ReportHandleRes struct{}
|
||||
type ReportViewReq struct {
|
||||
g.Meta `path:"/cultivate/reports/view" method:"get" tags:"养成管理" summary:"获取举报详情"`
|
||||
sysin.CultivateReportViewInp
|
||||
}
|
||||
type ReportViewRes struct {
|
||||
*sysin.CultivateReportViewModel
|
||||
}
|
||||
type LogListReq struct {
|
||||
g.Meta `path:"/cultivate/logs/list" method:"get" tags:"养成管理" summary:"获取养成管理操作日志"`
|
||||
sysin.CultivateOperationLogListInp
|
||||
}
|
||||
type LogListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.CultivateOperationLogListModel `json:"list"`
|
||||
}
|
||||
type DashboardReq struct {
|
||||
g.Meta `path:"/cultivate/dashboard" method:"get" tags:"养成管理" summary:"获取养成管理统计"`
|
||||
sysin.CultivateDashboardInp
|
||||
}
|
||||
type DashboardRes struct{ *sysin.CultivateDashboardModel }
|
||||
@@ -0,0 +1,33 @@
|
||||
package cultivateflower
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/cultivate/flowers/list" method:"get" tags:"养成管理" summary:"获取花种列表"`
|
||||
sysin.CultivateFlowerListInp
|
||||
}
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.CultivateFlowerListModel `json:"list"`
|
||||
}
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/cultivate/flowers/view" method:"get" tags:"养成管理" summary:"获取花种详情"`
|
||||
sysin.CultivateFlowerViewInp
|
||||
}
|
||||
type ViewRes struct {
|
||||
*sysin.CultivateFlowerViewModel
|
||||
}
|
||||
type EditReq struct {
|
||||
g.Meta `path:"/cultivate/flowers/edit" method:"post" tags:"养成管理" summary:"新建或编辑花种及阶段规则"`
|
||||
sysin.CultivateFlowerEditInp
|
||||
}
|
||||
type EditRes struct{}
|
||||
type StatusReq struct {
|
||||
g.Meta `path:"/cultivate/flowers/status" method:"post" tags:"养成管理" summary:"更新花种上下架状态"`
|
||||
sysin.CultivateFlowerStatusInp
|
||||
}
|
||||
type StatusRes struct{}
|
||||
@@ -1,6 +1,10 @@
|
||||
package imconfiginternal
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
import (
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type TenantReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/by-code/{code}" method:"get" tags:"IM内部配置" summary:"按编码获取IM租户"`
|
||||
@@ -58,6 +62,156 @@ type ThirdPartyRes struct {
|
||||
Values map[string]string `json:"values" dc:"配置值"`
|
||||
}
|
||||
|
||||
type DictionaryReq struct {
|
||||
g.Meta `path:"/internal/im-config/dictionary/{type}" method:"get" tags:"IM内部配置" summary:"获取字典配置"`
|
||||
Type string `json:"type" in:"path" dc:"字典类型"`
|
||||
}
|
||||
|
||||
type DictionaryItemModel struct {
|
||||
Label string `json:"label" dc:"展示名"`
|
||||
Value string `json:"value" dc:"配置值"`
|
||||
Sort int `json:"sort" dc:"排序"`
|
||||
}
|
||||
|
||||
type DictionaryRes struct {
|
||||
Items []*DictionaryItemModel `json:"items" dc:"字典项列表"`
|
||||
}
|
||||
|
||||
type ModerationProviderReq struct {
|
||||
g.Meta `path:"/internal/im-config/moderation/provider" method:"get" tags:"IM内部配置" summary:"获取内容审核服务配置"`
|
||||
}
|
||||
|
||||
type ModerationProviderRes struct {
|
||||
Provider string `json:"provider"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RegionID string `json:"region_id"`
|
||||
NicknameService string `json:"nickname_service"`
|
||||
ChatService string `json:"chat_service"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
FailureMode string `json:"failure_mode"`
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
AccessKeySecret string `json:"access_key_secret"`
|
||||
}
|
||||
|
||||
type ModerationPolicyReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{code}/moderation-policy" method:"get" tags:"IM内部配置" summary:"获取商户内容审核策略"`
|
||||
Code string `json:"code" in:"path" dc:"商户编码"`
|
||||
}
|
||||
|
||||
type ModerationPolicyRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AliyunEnabled bool `json:"aliyun_enabled"`
|
||||
LocalWordsEnabled bool `json:"local_words_enabled"`
|
||||
Scenes map[string]bool `json:"scenes"`
|
||||
RiskActions map[string]string `json:"risk_actions"`
|
||||
FailureMode string `json:"failure_mode"`
|
||||
BlockMessage string `json:"block_message"`
|
||||
}
|
||||
|
||||
type SecurityPolicyReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{code}/security-policy" method:"get" tags:"IM内部配置" summary:"获取商户安全策略"`
|
||||
Code string `json:"code" in:"path" dc:"商户编码"`
|
||||
}
|
||||
|
||||
type SecurityPolicyRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
GlobalFriendEnabled bool `json:"global_friend_enabled"`
|
||||
FriendDisabledPolicy map[string]interface{} `json:"friend_disabled_policy"`
|
||||
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
|
||||
LoginRateLimit *SecurityLoginRateLimitRes `json:"login_rate_limit"`
|
||||
CurfewRules []*SecurityCurfewRuleRes `json:"curfew_rules"`
|
||||
}
|
||||
|
||||
type RealnamePolicyReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/by-id/{id}/realname-policy" method:"get" tags:"IM内部配置" summary:"获取租户实名认证策略"`
|
||||
Id int64 `json:"id" in:"path" dc:"租户ID"`
|
||||
}
|
||||
|
||||
type RealnamePolicyRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
RealnameReviewPolicy int `json:"realname_review_policy"`
|
||||
}
|
||||
|
||||
type SecurityLoginRateLimitRes struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PasswordMaxAttempts int `json:"password_max_attempts"`
|
||||
LockSeconds int `json:"lock_seconds"`
|
||||
WindowSeconds int `json:"window_seconds"`
|
||||
}
|
||||
|
||||
type SecurityCurfewRuleRes struct {
|
||||
RuleCode string `json:"rule_code"`
|
||||
RuleName string `json:"rule_name"`
|
||||
DateType string `json:"date_type"`
|
||||
CustomDates []string `json:"custom_dates"`
|
||||
Weekdays []int `json:"weekdays"`
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Scope string `json:"scope"`
|
||||
WhitelistUserIds []string `json:"whitelist_user_ids"`
|
||||
WhitelistGroupIds []string `json:"whitelist_group_ids"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
type SensitiveWordsReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{code}/sensitive-words" method:"get" tags:"IM内部配置" summary:"获取商户有效敏感词"`
|
||||
Code string `json:"code" in:"path" dc:"商户编码"`
|
||||
}
|
||||
|
||||
type SensitiveWordsRes struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
Version int64 `json:"version"`
|
||||
Items []*SensitiveWordModel `json:"items"`
|
||||
}
|
||||
|
||||
type MessageKeyActiveReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/active" method:"get" tags:"IM内部配置" summary:"获取商户当前消息密钥"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
}
|
||||
|
||||
type MessageKeyActiveRes struct {
|
||||
*sysin.ImMessageKeyModel
|
||||
}
|
||||
|
||||
type MessageKeyVersionReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/{version}" method:"get" tags:"IM内部配置" summary:"获取商户指定版本消息密钥"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
Version uint `json:"version" in:"path"`
|
||||
}
|
||||
|
||||
type MessageKeyVersionRes struct {
|
||||
*sysin.ImMessageKeyModel
|
||||
}
|
||||
|
||||
type MessageKeyAckReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/{tenantCode}/message-keys/ack" method:"post" tags:"IM内部配置" summary:"确认商户消息密钥可用"`
|
||||
TenantCode string `json:"tenantCode" in:"path"`
|
||||
Version uint `json:"version" v:"required|min:1#密钥版本不能为空|密钥版本不正确"`
|
||||
}
|
||||
|
||||
type MessageKeyAckRes struct{}
|
||||
|
||||
type SensitiveWordModel struct {
|
||||
ID int64 `json:"id"`
|
||||
Word string `json:"word"`
|
||||
MatchType string `json:"match_type"`
|
||||
Scenes []string `json:"scenes"`
|
||||
Action string `json:"action"`
|
||||
Replacement string `json:"replacement"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
type TenantLoginIPWhitelistReq struct {
|
||||
g.Meta `path:"/internal/im-config/tenants/by-id/{id}/login-ip-whitelist" method:"get" tags:"IM内部配置" summary:"获取租户渠道登录IP白名单"`
|
||||
Id int64 `json:"id" in:"path" dc:"租户ID"`
|
||||
|
||||
@@ -263,83 +263,3 @@ type AdminLogListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupAdminLogListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ProfileUpdateReq 通过Kafka修改IM群资料
|
||||
type ProfileUpdateReq struct {
|
||||
g.Meta `path:"/imGroup/profileUpdate" method:"post" tags:"IM群组" summary:"Kafka修改IM群资料"`
|
||||
sysin.ImGroupProfileUpdateInp
|
||||
}
|
||||
|
||||
type ProfileUpdateRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// GovernanceUpdateReq 通过Kafka修改IM群治理配置
|
||||
type GovernanceUpdateReq struct {
|
||||
g.Meta `path:"/imGroup/governanceUpdate" method:"post" tags:"IM群组" summary:"Kafka修改IM群治理配置"`
|
||||
sysin.ImGroupGovernanceUpdateInp
|
||||
}
|
||||
|
||||
type GovernanceUpdateRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// AdminSetReq 通过Kafka设置IM群管理员
|
||||
type AdminSetReq struct {
|
||||
g.Meta `path:"/imGroup/adminSet" method:"post" tags:"IM群组" summary:"Kafka设置IM群管理员"`
|
||||
sysin.ImGroupAdminSetInp
|
||||
}
|
||||
|
||||
type AdminSetRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// AdminRemoveReq 通过Kafka删除IM群管理员
|
||||
type AdminRemoveReq struct {
|
||||
g.Meta `path:"/imGroup/adminRemove" method:"post" tags:"IM群组" summary:"Kafka删除IM群管理员"`
|
||||
sysin.ImGroupAdminRemoveInp
|
||||
}
|
||||
|
||||
type AdminRemoveRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// MemberMuteReq 通过Kafka禁言IM群成员
|
||||
type MemberMuteReq struct {
|
||||
g.Meta `path:"/imGroup/memberMute" method:"post" tags:"IM群组" summary:"Kafka禁言IM群成员"`
|
||||
sysin.ImGroupMemberMuteInp
|
||||
}
|
||||
|
||||
type MemberMuteRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// MemberUnmuteReq 通过Kafka解除IM群成员禁言
|
||||
type MemberUnmuteReq struct {
|
||||
g.Meta `path:"/imGroup/memberUnmute" method:"post" tags:"IM群组" summary:"Kafka解除IM群成员禁言"`
|
||||
sysin.ImGroupMemberUnmuteInp
|
||||
}
|
||||
|
||||
type MemberUnmuteRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// MemberRemoveReq 通过Kafka移出IM群成员
|
||||
type MemberRemoveReq struct {
|
||||
g.Meta `path:"/imGroup/memberRemove" method:"post" tags:"IM群组" summary:"Kafka移出IM群成员"`
|
||||
sysin.ImGroupMemberRemoveInp
|
||||
}
|
||||
|
||||
type MemberRemoveRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// OwnerTransferReq 通过Kafka转让IM群主
|
||||
type OwnerTransferReq struct {
|
||||
g.Meta `path:"/imGroup/ownerTransfer" method:"post" tags:"IM群组" summary:"Kafka转让IM群主"`
|
||||
sysin.ImGroupOwnerTransferInp
|
||||
}
|
||||
|
||||
type OwnerTransferRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
@@ -35,3 +35,8 @@ type BanUserReq struct {
|
||||
sysin.ImLoginFailureBanUserInp
|
||||
}
|
||||
type BanUserRes struct{ *sysin.ImUserCommandModel }
|
||||
type UnlockReq struct {
|
||||
g.Meta `path:"/imLoginFailureLog/unlock" method:"post" tags:"IM登录失败日志" summary:"解锁登录失败锁定"`
|
||||
sysin.ImLoginFailureUnlockInp
|
||||
}
|
||||
type UnlockRes struct{}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package immessageencryption
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/list" method:"get" tags:"消息加密密钥" summary:"获取商户消息加密状态"`
|
||||
sysin.ImMessageEncryptionListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImMessageEncryptionListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
type InitializeReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/initialize" method:"post" tags:"消息加密密钥" summary:"初始化商户消息密钥"`
|
||||
sysin.ImMessageEncryptionActionInp
|
||||
}
|
||||
type InitializeRes struct{}
|
||||
|
||||
type RotateReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/rotate" method:"post" tags:"消息加密密钥" summary:"轮换商户消息密钥"`
|
||||
sysin.ImMessageEncryptionActionInp
|
||||
}
|
||||
type RotateRes struct{}
|
||||
|
||||
type SwitchReq struct {
|
||||
g.Meta `path:"/imMessageEncryption/switch" method:"post" tags:"消息加密密钥" summary:"切换商户消息加密"`
|
||||
sysin.ImMessageEncryptionSwitchInp
|
||||
}
|
||||
type SwitchRes struct{}
|
||||
@@ -39,6 +39,16 @@ type RestoreDefaultReq struct {
|
||||
type RestoreDefaultRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
type LoginLockoutConfigGetReq struct {
|
||||
g.Meta `path:"/imSecurityPolicy/loginLockoutConfig" method:"get" tags:"IM安全策略" summary:"获取登录失败策略配置"`
|
||||
sysin.ImLoginLockoutConfigGetInp
|
||||
}
|
||||
type LoginLockoutConfigGetRes struct{ *sysin.ImLoginLockoutConfigModel }
|
||||
type LoginLockoutConfigSaveReq struct {
|
||||
g.Meta `path:"/imSecurityPolicy/loginLockoutConfig" method:"post" tags:"IM安全策略" summary:"保存登录失败策略配置"`
|
||||
sysin.ImLoginLockoutConfigSaveInp
|
||||
}
|
||||
type LoginLockoutConfigSaveRes struct{}
|
||||
type IPRuleListReq struct {
|
||||
g.Meta `path:"/imSecurityPolicy/ipRuleList" method:"get" tags:"IM安全策略" summary:"获取IP黑白名单"`
|
||||
sysin.ImSecurityIPRuleListInp
|
||||
|
||||
@@ -33,9 +33,17 @@ type ViewRes struct {
|
||||
*sysin.ImTenantViewModel
|
||||
}
|
||||
|
||||
// EditReq 修改/新增IM租户
|
||||
// AddReq 新增IM租户
|
||||
type AddReq struct {
|
||||
g.Meta `path:"/imTenant/add" method:"post" tags:"IM租户" summary:"新增IM租户"`
|
||||
sysin.ImTenantEditInp
|
||||
}
|
||||
|
||||
type AddRes struct{}
|
||||
|
||||
// EditReq 修改IM租户
|
||||
type EditReq struct {
|
||||
g.Meta `path:"/imTenant/edit" method:"post" tags:"IM租户" summary:"修改/新增IM租户"`
|
||||
g.Meta `path:"/imTenant/edit" method:"post" tags:"IM租户" summary:"修改IM租户"`
|
||||
sysin.ImTenantEditInp
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,13 @@ type AdjustGroupReq struct {
|
||||
|
||||
type AdjustGroupRes struct{}
|
||||
|
||||
type FriendAddReq struct {
|
||||
g.Meta `path:"/imUser/friendAdd" method:"post" tags:"IM用户" summary:"设置IM用户加好友开关"`
|
||||
sysin.ImUserFriendAddInp
|
||||
}
|
||||
|
||||
type FriendAddRes struct{}
|
||||
|
||||
type IPWhitelistReq struct {
|
||||
g.Meta `path:"/imUser/ipWhitelist" method:"get" tags:"IM用户" summary:"获取IM用户IP白名单"`
|
||||
sysin.ImUserIPWhitelistInp
|
||||
|
||||
+4
-3
@@ -4,10 +4,11 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/IBM/sarama v1.45.2
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6
|
||||
github.com/alibabacloud-go/tea v1.3.9
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/alibabacloud-go/green-20220302/v3 v3.5.4
|
||||
github.com/alibabacloud-go/tea v1.5.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/apache/rocketmq-client-go/v2 v2.1.2
|
||||
github.com/casbin/casbin/v2 v2.108.0
|
||||
|
||||
+8
-7
@@ -23,8 +23,8 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7 h1:ASXSBga98QrGMxbIThCD6jAti09gedLfvry6yJtsoBE=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7/go.mod h1:TBpgqm3XofZz2LCYjZhektGPU7ArEgascyzbm4SjFo4=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 h1:o6veen0IZ/Fe1JawwhwQMZcbw67CVDY1pQwXcNWPyQo=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
@@ -37,6 +37,8 @@ github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 h1:UTl97mt2qfavxveqCkaVg4
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6/go.mod h1:UWpcGrWwTbES9QW7OQ7xDffukMJ/l7lzioixIz8+lgY=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/green-20220302/v3 v3.5.4 h1:ZnlHri7tTLl28JWB5S2J6MYvD0ewJFI7hedK4E7+gSk=
|
||||
github.com/alibabacloud-go/green-20220302/v3 v3.5.4/go.mod h1:PQTtoEuWOsuq7H7sgTRcXDw4Ut/Hekgy5z2mWGtO9OQ=
|
||||
github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
@@ -48,16 +50,15 @@ github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy
|
||||
github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.8/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea v1.3.9 h1:bjgt1bvdY780vz/17iWNNtbXl4A77HWntWMeaUF3So0=
|
||||
github.com/alibabacloud-go/tea v1.3.9/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea v1.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
|
||||
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
|
||||
@@ -34,6 +34,16 @@ gfcli:
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
|
||||
- link: "mysql:root:root@tcp(127.0.0.1:3306)/cultivate?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
group: "cultivate"
|
||||
tables: "cv_flower_catalog,cv_flower_stage_rule,cv_feed_post,cv_feed_media,cv_feed_comment,cv_feed_report,cv_content_audit_log,cv_admin_operation_log"
|
||||
removePrefix: "cv_"
|
||||
descriptionTag: true
|
||||
noModelComment: true
|
||||
jsonCase: "CamelLower"
|
||||
gJsonSupport: true
|
||||
clear: false
|
||||
|
||||
|
||||
# service: # 生成业务配置
|
||||
# srcFolder: "internal/logic"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
CultivateFlowerOnline = "online"
|
||||
CultivateFlowerOffline = "offline"
|
||||
CultivateFlowerActivityOnly = "activity_only"
|
||||
CultivateFlowerMaintenance = "maintenance"
|
||||
)
|
||||
|
||||
var CultivateFlowerStatusOptions = []*model.Option{
|
||||
dict.GenSuccessOption(CultivateFlowerOnline, "已上架"),
|
||||
dict.GenDefaultOption(CultivateFlowerOffline, "已下架"),
|
||||
dict.GenPrimaryOption(CultivateFlowerActivityOnly, "仅活动"),
|
||||
dict.GenWarningOption(CultivateFlowerMaintenance, "维护中"),
|
||||
}
|
||||
|
||||
var CultivateContentStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption("reviewing", "待审核"),
|
||||
dict.GenSuccessOption("published", "已发布"),
|
||||
dict.GenErrorOption("rejected", "已拒绝"),
|
||||
dict.GenDefaultOption("taken_down", "已下架"),
|
||||
dict.GenDefaultOption("deleted", "已删除"),
|
||||
}
|
||||
|
||||
var CultivateReportStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption("submitted", "待处理"),
|
||||
dict.GenPrimaryOption("processing", "处理中"),
|
||||
dict.GenSuccessOption("resolved", "已处理"),
|
||||
dict.GenDefaultOption("rejected", "已驳回"),
|
||||
}
|
||||
|
||||
var CultivateContentTypeOptions = []*model.Option{
|
||||
dict.GenPrimaryOption("feed_post", "动态"),
|
||||
dict.GenInfoOption("feed_comment", "评论"),
|
||||
}
|
||||
|
||||
var CultivateReportObjectTypeOptions = []*model.Option{
|
||||
dict.GenPrimaryOption("feed_post", "动态"),
|
||||
dict.GenInfoOption("feed_comment", "评论"),
|
||||
dict.GenDefaultOption("user", "用户"),
|
||||
}
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("CultivateFlowerStatusOptions", "养成花种状态", CultivateFlowerStatusOptions)
|
||||
dict.RegisterEnums("CultivateContentStatusOptions", "养成内容状态", CultivateContentStatusOptions)
|
||||
dict.RegisterEnums("CultivateReportStatusOptions", "养成举报状态", CultivateReportStatusOptions)
|
||||
dict.RegisterEnums("CultivateContentTypeOptions", "养成内容类型", CultivateContentTypeOptions)
|
||||
dict.RegisterEnums("CultivateReportObjectTypeOptions", "养成举报对象类型", CultivateReportObjectTypeOptions)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
RegionSpilt = " / " // 地区分隔符
|
||||
Unknown = "Unknown" // Unknown
|
||||
SuperRoleKey = "super" // 超管角色唯一标识符,通过角色验证超管
|
||||
ImPlatformRoleKey = "supreme_manager" // IM平台超管角色唯一标识符
|
||||
MaxServeLogContentLen = 2048 // 最大保留服务日志内容大小
|
||||
SysDefaultLanguage = "zh_CN" // 系统默认语言,当配置文件没有国际化配置时生效
|
||||
)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
ImInternalSecretDictType = "im_internal_secret"
|
||||
ImMessageRootKEKLabel = "core_material_01"
|
||||
|
||||
ImMessageKeyAlgorithm = "AES-256-GCM"
|
||||
ImMessageKeyStatusPending = "pending"
|
||||
ImMessageKeyStatusActive = "active"
|
||||
ImMessageKeyStatusRetired = "retired"
|
||||
ImMessageKeyStatusDisabled = "disabled"
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/cultivatecontent"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var CultivateContent = cCultivateContent{}
|
||||
|
||||
type cCultivateContent struct{}
|
||||
|
||||
func (*cCultivateContent) Dashboard(ctx context.Context, req *cultivatecontent.DashboardReq) (res *cultivatecontent.DashboardRes, err error) {
|
||||
data, err := service.SysCultivateContent().Dashboard(ctx, &req.CultivateDashboardInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cultivatecontent.DashboardRes{CultivateDashboardModel: data}, nil
|
||||
}
|
||||
|
||||
func (*cCultivateContent) List(ctx context.Context, req *cultivatecontent.ListReq) (res *cultivatecontent.ListRes, err error) {
|
||||
list, total, err := service.SysCultivateContent().List(ctx, &req.CultivateContentListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.CultivateContentListModel{}
|
||||
}
|
||||
res = &cultivatecontent.ListRes{List: list}
|
||||
res.PageRes.Pack(req, total)
|
||||
return
|
||||
}
|
||||
func (*cCultivateContent) View(ctx context.Context, req *cultivatecontent.ViewReq) (res *cultivatecontent.ViewRes, err error) {
|
||||
data, err := service.SysCultivateContent().View(ctx, &req.CultivateContentViewInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cultivatecontent.ViewRes{CultivateContentViewModel: data}, nil
|
||||
}
|
||||
func (*cCultivateContent) Review(ctx context.Context, req *cultivatecontent.ReviewReq) (res *cultivatecontent.ReviewRes, err error) {
|
||||
err = service.SysCultivateContent().Review(ctx, &req.CultivateContentReviewInp)
|
||||
return
|
||||
}
|
||||
func (*cCultivateContent) BatchReview(ctx context.Context, req *cultivatecontent.BatchReviewReq) (res *cultivatecontent.BatchReviewRes, err error) {
|
||||
data, err := service.SysCultivateContent().BatchReview(ctx, &req.CultivateContentBatchReviewInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cultivatecontent.BatchReviewRes{CultivateContentBatchReviewModel: data}, nil
|
||||
}
|
||||
func (*cCultivateContent) ReportList(ctx context.Context, req *cultivatecontent.ReportListReq) (res *cultivatecontent.ReportListRes, err error) {
|
||||
list, total, err := service.SysCultivateContent().ReportList(ctx, &req.CultivateReportListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.CultivateReportListModel{}
|
||||
}
|
||||
res = &cultivatecontent.ReportListRes{List: list}
|
||||
res.PageRes.Pack(req, total)
|
||||
return
|
||||
}
|
||||
func (*cCultivateContent) ReportHandle(ctx context.Context, req *cultivatecontent.ReportHandleReq) (res *cultivatecontent.ReportHandleRes, err error) {
|
||||
err = service.SysCultivateContent().ReportHandle(ctx, &req.CultivateReportHandleInp)
|
||||
return
|
||||
}
|
||||
func (*cCultivateContent) ReportView(ctx context.Context, req *cultivatecontent.ReportViewReq) (res *cultivatecontent.ReportViewRes, err error) {
|
||||
data, err := service.SysCultivateContent().ReportView(ctx, &req.CultivateReportViewInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cultivatecontent.ReportViewRes{CultivateReportViewModel: data}, nil
|
||||
}
|
||||
func (*cCultivateContent) LogList(ctx context.Context, req *cultivatecontent.LogListReq) (res *cultivatecontent.LogListRes, err error) {
|
||||
list, total, err := service.SysCultivateContent().LogList(ctx, &req.CultivateOperationLogListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.CultivateOperationLogListModel{}
|
||||
}
|
||||
res = &cultivatecontent.LogListRes{List: list}
|
||||
res.PageRes.Pack(req, total)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/cultivateflower"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var CultivateFlower = cCultivateFlower{}
|
||||
|
||||
type cCultivateFlower struct{}
|
||||
|
||||
func (*cCultivateFlower) List(ctx context.Context, req *cultivateflower.ListReq) (res *cultivateflower.ListRes, err error) {
|
||||
list, total, err := service.SysCultivateFlower().List(ctx, &req.CultivateFlowerListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.CultivateFlowerListModel{}
|
||||
}
|
||||
res = &cultivateflower.ListRes{List: list}
|
||||
res.PageRes.Pack(req, total)
|
||||
return
|
||||
}
|
||||
func (*cCultivateFlower) View(ctx context.Context, req *cultivateflower.ViewReq) (res *cultivateflower.ViewRes, err error) {
|
||||
data, err := service.SysCultivateFlower().View(ctx, &req.CultivateFlowerViewInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cultivateflower.ViewRes{CultivateFlowerViewModel: data}, nil
|
||||
}
|
||||
func (*cCultivateFlower) Edit(ctx context.Context, req *cultivateflower.EditReq) (res *cultivateflower.EditRes, err error) {
|
||||
err = service.SysCultivateFlower().Edit(ctx, &req.CultivateFlowerEditInp)
|
||||
return
|
||||
}
|
||||
func (*cCultivateFlower) Status(ctx context.Context, req *cultivateflower.StatusReq) (res *cultivateflower.StatusRes, err error) {
|
||||
err = service.SysCultivateFlower().Status(ctx, &req.CultivateFlowerStatusInp)
|
||||
return
|
||||
}
|
||||
@@ -314,91 +314,3 @@ func (c *cImGroup) AdminLogList(ctx context.Context, req *imgroup.AdminLogListRe
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// ProfileUpdate 通过Kafka修改IM群资料
|
||||
func (c *cImGroup) ProfileUpdate(ctx context.Context, req *imgroup.ProfileUpdateReq) (res *imgroup.ProfileUpdateRes, err error) {
|
||||
data, err := service.SysImGroup().ProfileUpdate(ctx, &req.ImGroupProfileUpdateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.ProfileUpdateRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// GovernanceUpdate 通过Kafka修改IM群治理配置
|
||||
func (c *cImGroup) GovernanceUpdate(ctx context.Context, req *imgroup.GovernanceUpdateReq) (res *imgroup.GovernanceUpdateRes, err error) {
|
||||
data, err := service.SysImGroup().GovernanceUpdate(ctx, &req.ImGroupGovernanceUpdateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.GovernanceUpdateRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// AdminSet 通过Kafka设置IM群管理员
|
||||
func (c *cImGroup) AdminSet(ctx context.Context, req *imgroup.AdminSetReq) (res *imgroup.AdminSetRes, err error) {
|
||||
data, err := service.SysImGroup().AdminSet(ctx, &req.ImGroupAdminSetInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.AdminSetRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// AdminRemove 通过Kafka删除IM群管理员
|
||||
func (c *cImGroup) AdminRemove(ctx context.Context, req *imgroup.AdminRemoveReq) (res *imgroup.AdminRemoveRes, err error) {
|
||||
data, err := service.SysImGroup().AdminRemove(ctx, &req.ImGroupAdminRemoveInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.AdminRemoveRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// MemberMute 通过Kafka禁言IM群成员
|
||||
func (c *cImGroup) MemberMute(ctx context.Context, req *imgroup.MemberMuteReq) (res *imgroup.MemberMuteRes, err error) {
|
||||
data, err := service.SysImGroup().MemberMute(ctx, &req.ImGroupMemberMuteInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.MemberMuteRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// MemberUnmute 通过Kafka解除IM群成员禁言
|
||||
func (c *cImGroup) MemberUnmute(ctx context.Context, req *imgroup.MemberUnmuteReq) (res *imgroup.MemberUnmuteRes, err error) {
|
||||
data, err := service.SysImGroup().MemberUnmute(ctx, &req.ImGroupMemberUnmuteInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.MemberUnmuteRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// MemberRemove 通过Kafka移出IM群成员
|
||||
func (c *cImGroup) MemberRemove(ctx context.Context, req *imgroup.MemberRemoveReq) (res *imgroup.MemberRemoveRes, err error) {
|
||||
data, err := service.SysImGroup().MemberRemove(ctx, &req.ImGroupMemberRemoveInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.MemberRemoveRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// OwnerTransfer 通过Kafka转让IM群主
|
||||
func (c *cImGroup) OwnerTransfer(ctx context.Context, req *imgroup.OwnerTransferReq) (res *imgroup.OwnerTransferRes, err error) {
|
||||
data, err := service.SysImGroup().OwnerTransfer(ctx, &req.ImGroupOwnerTransferInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.OwnerTransferRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
@@ -37,3 +37,6 @@ func (c *cImLoginFailureLog) BanUser(ctx context.Context, req *imloginfailurelog
|
||||
data, err := service.SysImLoginFailureLog().BanUser(ctx, &req.ImLoginFailureBanUserInp)
|
||||
return &imloginfailurelog.BanUserRes{ImUserCommandModel: data}, err
|
||||
}
|
||||
func (c *cImLoginFailureLog) Unlock(ctx context.Context, req *imloginfailurelog.UnlockReq) (*imloginfailurelog.UnlockRes, error) {
|
||||
return &imloginfailurelog.UnlockRes{}, service.SysImLoginFailureLog().Unlock(ctx, &req.ImLoginFailureUnlockInp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/immessageencryption"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var ImMessageEncryption = cImMessageEncryption{}
|
||||
|
||||
type cImMessageEncryption struct{}
|
||||
|
||||
func (c *cImMessageEncryption) List(ctx context.Context, req *immessageencryption.ListReq) (res *immessageencryption.ListRes, err error) {
|
||||
list, total, err := service.SysImMessageEncryption().List(ctx, &req.ImMessageEncryptionListInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImMessageEncryptionListModel{}
|
||||
}
|
||||
res = new(immessageencryption.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, total)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Initialize(ctx context.Context, req *immessageencryption.InitializeReq) (res *immessageencryption.InitializeRes, err error) {
|
||||
err = service.SysImMessageEncryption().Initialize(ctx, &req.ImMessageEncryptionActionInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Rotate(ctx context.Context, req *immessageencryption.RotateReq) (res *immessageencryption.RotateRes, err error) {
|
||||
err = service.SysImMessageEncryption().Rotate(ctx, &req.ImMessageEncryptionActionInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImMessageEncryption) Switch(ctx context.Context, req *immessageencryption.SwitchReq) (res *immessageencryption.SwitchRes, err error) {
|
||||
err = service.SysImMessageEncryption().Switch(ctx, &req.ImMessageEncryptionSwitchInp)
|
||||
return
|
||||
}
|
||||
@@ -49,6 +49,17 @@ func (c *cImSecurityPolicy) RestoreDefault(ctx context.Context, req *imsecurityp
|
||||
}
|
||||
return &imsecuritypolicy.RestoreDefaultRes{Id: id}, nil
|
||||
}
|
||||
func (c *cImSecurityPolicy) GetLoginLockoutConfig(ctx context.Context, req *imsecuritypolicy.LoginLockoutConfigGetReq) (res *imsecuritypolicy.LoginLockoutConfigGetRes, err error) {
|
||||
data, err := service.SysImSecurityPolicy().GetLoginLockoutConfig(ctx, &req.ImLoginLockoutConfigGetInp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imsecuritypolicy.LoginLockoutConfigGetRes{ImLoginLockoutConfigModel: data}, nil
|
||||
}
|
||||
func (c *cImSecurityPolicy) SaveLoginLockoutConfig(ctx context.Context, req *imsecuritypolicy.LoginLockoutConfigSaveReq) (res *imsecuritypolicy.LoginLockoutConfigSaveRes, err error) {
|
||||
err = service.SysImSecurityPolicy().SaveLoginLockoutConfig(ctx, &req.ImLoginLockoutConfigSaveInp)
|
||||
return
|
||||
}
|
||||
func (c *cImSecurityPolicy) IPRuleList(ctx context.Context, req *imsecuritypolicy.IPRuleListReq) (res *imsecuritypolicy.IPRuleListRes, err error) {
|
||||
list, total, err := service.SysImSecurityPolicy().IPRuleList(ctx, &req.ImSecurityIPRuleListInp)
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,12 @@ func (c *cImTenant) List(ctx context.Context, req *imtenant.ListReq) (res *imten
|
||||
return
|
||||
}
|
||||
|
||||
// Add 新增IM租户
|
||||
func (c *cImTenant) Add(ctx context.Context, req *imtenant.AddReq) (res *imtenant.AddRes, err error) {
|
||||
err = service.SysImTenant().Add(ctx, &req.ImTenantEditInp)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 更新IM租户
|
||||
func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) {
|
||||
err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp)
|
||||
|
||||
@@ -108,6 +108,11 @@ func (c *cImUser) AdjustGroup(ctx context.Context, req *imuser.AdjustGroupReq) (
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) FriendAdd(ctx context.Context, req *imuser.FriendAddReq) (res *imuser.FriendAddRes, err error) {
|
||||
err = service.SysImUser().FriendAdd(ctx, &req.ImUserFriendAddInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) IPWhitelist(ctx context.Context, req *imuser.IPWhitelistReq) (res *imuser.IPWhitelistRes, err error) {
|
||||
data, err := service.SysImUser().IPWhitelist(ctx, &req.ImUserIPWhitelistInp)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func Serve(r *ghttp.Request) {
|
||||
checks := map[string]string{}
|
||||
statusCode := http.StatusOK
|
||||
|
||||
if err := g.DB().Ctx(ctx).Raw("SELECT 1").Scan(new(int)); err != nil {
|
||||
if _, err := g.DB().Ctx(ctx).Raw("SELECT 1").One(); err != nil {
|
||||
checks["mysql"] = err.Error()
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
} else {
|
||||
|
||||
@@ -2,10 +2,14 @@ package imconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
api "hotgo/api/admin/imconfiginternal"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/immoderation"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
|
||||
@@ -14,6 +18,18 @@ import (
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const defaultModerationTenantCode = "default"
|
||||
|
||||
var defaultModerationScenes = map[string]bool{
|
||||
"user_nickname": true, "user_signature": true, "group_name": true,
|
||||
"group_nickname": true, "private_message": true, "group_message": true,
|
||||
"group_announcement": true,
|
||||
}
|
||||
|
||||
var defaultModerationRiskActions = map[string]string{
|
||||
"high": "block", "medium": "block", "low": "audit_only", "none": "allow",
|
||||
}
|
||||
|
||||
var Config = cConfig{}
|
||||
|
||||
type cConfig struct{}
|
||||
@@ -91,6 +107,375 @@ func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *
|
||||
return &api.ThirdPartyRes{Values: values}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) Dictionary(ctx context.Context, req *api.DictionaryReq) (res *api.DictionaryRes, err error) {
|
||||
dictType := strings.TrimSpace(req.Type)
|
||||
if dictType == "" || dictType == consts.ImInternalSecretDictType {
|
||||
return nil, gerror.New("字典类型不合法")
|
||||
}
|
||||
var rows []*entity.SysDictData
|
||||
if err = dao.SysDictData.Ctx(ctx).
|
||||
Where(dao.SysDictData.Columns().Type, dictType).
|
||||
Where(dao.SysDictData.Columns().Status, consts.StatusEnabled).
|
||||
Order(dao.SysDictData.Columns().Sort + " asc, " + dao.SysDictData.Columns().Id + " desc").
|
||||
Scan(&rows); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM字典配置失败")
|
||||
}
|
||||
res = &api.DictionaryRes{Items: make([]*api.DictionaryItemModel, 0, len(rows))}
|
||||
for _, row := range rows {
|
||||
res.Items = append(res.Items, &api.DictionaryItemModel{
|
||||
Label: row.Label,
|
||||
Value: row.Value,
|
||||
Sort: row.Sort,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) ModerationProvider(ctx context.Context, req *api.ModerationProviderReq) (res *api.ModerationProviderRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, defaultModerationTenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var row struct {
|
||||
Provider string `json:"provider"`
|
||||
Enabled int `json:"enabled"`
|
||||
ConfigJSON string `json:"config_json"`
|
||||
}
|
||||
record, err := imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||
Fields("provider", "enabled", "CAST(config_json AS CHAR) config_json").
|
||||
Where("tenant_id", tenant.Id).Where("service", consts.ImExternalServiceModeration).One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取内容审核服务配置失败")
|
||||
}
|
||||
if !record.IsEmpty() {
|
||||
if err = record.Struct(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析内容审核服务配置失败")
|
||||
}
|
||||
}
|
||||
globalConfig, err := immoderation.LoadAliyunGlobalConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(row.ConfigJSON), &config)
|
||||
res = &api.ModerationProviderRes{
|
||||
Provider: stringValue(map[string]any{"provider": row.Provider}, "provider", "aliyun_green"),
|
||||
Endpoint: globalConfig.Endpoint, Enabled: row.Enabled == 1,
|
||||
RegionID: globalConfig.RegionID,
|
||||
NicknameService: stringValue(config, "nickname_service", "nickname_detection_pro"),
|
||||
ChatService: stringValue(config, "chat_service", "chat_detection_pro"),
|
||||
TimeoutMS: intValue(config, "timeout_ms", 1500),
|
||||
FailureMode: moderationFailureMode(stringValue(config, "failure_mode", "allow")),
|
||||
AccessKeyID: globalConfig.AccessKeyID,
|
||||
AccessKeySecret: globalConfig.AccessKeySecret,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) ModerationPolicy(ctx context.Context, req *api.ModerationPolicyReq) (res *api.ModerationPolicyRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = defaultModerationPolicy(tenant)
|
||||
var tenantConfig struct {
|
||||
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
|
||||
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
|
||||
columns := dao.ImSecurityPolicy.Columns()
|
||||
var row struct {
|
||||
PolicyVersion uint `json:"policy_version"`
|
||||
SensitiveWordPolicy string `json:"sensitive_word_policy"`
|
||||
}
|
||||
record, err := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(columns.PolicyVersion, "CAST("+columns.SensitiveWordPolicy+" AS CHAR) sensitive_word_policy").
|
||||
Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户内容审核策略失败")
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return res, nil
|
||||
}
|
||||
if err = record.Struct(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析商户内容审核策略失败")
|
||||
}
|
||||
if row.PolicyVersion == 0 || strings.TrimSpace(row.SensitiveWordPolicy) == "" {
|
||||
return res, nil
|
||||
}
|
||||
var policy struct {
|
||||
Scenes map[string]bool `json:"scenes"`
|
||||
RiskActions map[string]string `json:"risk_actions"`
|
||||
FailureMode string `json:"failure_mode"`
|
||||
BlockMessage string `json:"block_message"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(row.SensitiveWordPolicy), &policy); err != nil {
|
||||
return nil, gerror.Wrap(err, "商户内容审核策略格式错误")
|
||||
}
|
||||
res.PolicyVersion = row.PolicyVersion
|
||||
for scene, enabled := range policy.Scenes {
|
||||
res.Scenes[scene] = enabled
|
||||
}
|
||||
for risk, action := range policy.RiskActions {
|
||||
if action == "allow" || action == "block" || action == "audit_only" {
|
||||
res.RiskActions[risk] = action
|
||||
}
|
||||
}
|
||||
res.FailureMode = moderationFailureMode(policy.FailureMode)
|
||||
if strings.TrimSpace(policy.BlockMessage) != "" {
|
||||
res.BlockMessage = strings.TrimSpace(policy.BlockMessage)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) SecurityPolicy(ctx context.Context, req *api.SecurityPolicyReq) (res *api.SecurityPolicyRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &api.SecurityPolicyRes{
|
||||
TenantID: tenant.Id,
|
||||
TenantCode: tenant.Code,
|
||||
GlobalFriendEnabled: true,
|
||||
FriendDisabledPolicy: map[string]interface{}{},
|
||||
MessageCurfewEnabled: false,
|
||||
CurfewRules: []*api.SecurityCurfewRuleRes{},
|
||||
}
|
||||
columns := dao.ImSecurityPolicy.Columns()
|
||||
record, err := imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(columns.Id, columns.PolicyVersion, columns.GlobalFriendEnabled, columns.MessageCurfewEnabled, "CAST("+columns.LoginRateLimit+" AS CHAR) login_rate_limit", "CAST("+columns.FriendDisabledPolicy+" AS CHAR) friend_disabled_policy").
|
||||
Where(columns.TenantId, tenant.Id).Where(columns.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(columns.DeletedAt).OrderDesc(columns.PolicyVersion).One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户安全策略失败")
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return res, nil
|
||||
}
|
||||
policyID := record[columns.Id].Int64()
|
||||
res.PolicyVersion = record[columns.PolicyVersion].Uint()
|
||||
res.GlobalFriendEnabled = record[columns.GlobalFriendEnabled].Int() == 1
|
||||
res.MessageCurfewEnabled = record[columns.MessageCurfewEnabled].Int() == 1
|
||||
if raw := strings.TrimSpace(record["login_rate_limit"].String()); raw != "" && raw != "null" {
|
||||
var limit struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PasswordMaxAttempts int `json:"password_max_attempts"`
|
||||
LockSeconds int `json:"lock_seconds"`
|
||||
WindowSeconds int `json:"window_seconds"`
|
||||
}
|
||||
if json.Unmarshal([]byte(raw), &limit) == nil && limit.Enabled {
|
||||
res.LoginRateLimit = &api.SecurityLoginRateLimitRes{
|
||||
Enabled: true,
|
||||
PasswordMaxAttempts: limit.PasswordMaxAttempts,
|
||||
LockSeconds: limit.LockSeconds,
|
||||
WindowSeconds: limit.WindowSeconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(record["friend_disabled_policy"].String()); raw != "" && raw != "null" {
|
||||
_ = json.Unmarshal([]byte(raw), &res.FriendDisabledPolicy)
|
||||
}
|
||||
ruleColumns := dao.ImMessageCurfewRules.Columns()
|
||||
var rows []struct {
|
||||
RuleCode string `json:"rule_code"`
|
||||
RuleName string `json:"rule_name"`
|
||||
DateType string `json:"date_type"`
|
||||
CustomDates string `json:"custom_dates"`
|
||||
Weekdays string `json:"weekdays"`
|
||||
Timezone string `json:"timezone"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Scope string `json:"scope"`
|
||||
WhitelistUserIds string `json:"whitelist_user_ids"`
|
||||
WhitelistGroupIds string `json:"whitelist_group_ids"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).
|
||||
Fields(ruleColumns.RuleCode, ruleColumns.RuleName, ruleColumns.DateType, "CAST("+ruleColumns.CustomDates+" AS CHAR) custom_dates", "CAST("+ruleColumns.Weekdays+" AS CHAR) weekdays", ruleColumns.Timezone, "TIME_FORMAT("+ruleColumns.StartTime+",'%H:%i') start_time", "TIME_FORMAT("+ruleColumns.EndTime+",'%H:%i') end_time", ruleColumns.Scope, "CAST("+ruleColumns.WhitelistUserIds+" AS CHAR) whitelist_user_ids", "CAST("+ruleColumns.WhitelistGroupIds+" AS CHAR) whitelist_group_ids", ruleColumns.Message, ruleColumns.Status, ruleColumns.Sort).
|
||||
Where(ruleColumns.PolicyId, policyID).Where(ruleColumns.Status, consts.StatusEnabled).
|
||||
WhereNull(ruleColumns.DeletedAt).OrderAsc(ruleColumns.Sort).OrderAsc(ruleColumns.Id).Scan(&rows); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户宵禁规则失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
res.CurfewRules = append(res.CurfewRules, &api.SecurityCurfewRuleRes{
|
||||
RuleCode: row.RuleCode,
|
||||
RuleName: row.RuleName,
|
||||
DateType: row.DateType,
|
||||
CustomDates: parseStringArray(row.CustomDates),
|
||||
Weekdays: parseIntArray(row.Weekdays),
|
||||
Timezone: row.Timezone,
|
||||
StartTime: row.StartTime,
|
||||
EndTime: row.EndTime,
|
||||
Scope: row.Scope,
|
||||
WhitelistUserIds: parseStringArray(row.WhitelistUserIds),
|
||||
WhitelistGroupIds: parseStringArray(row.WhitelistGroupIds),
|
||||
Message: row.Message,
|
||||
Status: row.Status,
|
||||
Sort: row.Sort,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) RealnamePolicy(ctx context.Context, req *api.RealnamePolicyReq) (res *api.RealnamePolicyRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Id, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = defaultRealnamePolicy(tenant.Id)
|
||||
var row *struct {
|
||||
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
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseStringArray(raw string) []string {
|
||||
values := []string{}
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return values
|
||||
}
|
||||
_ = json.Unmarshal([]byte(raw), &values)
|
||||
return values
|
||||
}
|
||||
|
||||
func parseIntArray(raw string) []int {
|
||||
values := []int{}
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return values
|
||||
}
|
||||
_ = json.Unmarshal([]byte(raw), &values)
|
||||
return values
|
||||
}
|
||||
|
||||
func (c *cConfig) SensitiveWords(ctx context.Context, req *api.SensitiveWordsReq) (res *api.SensitiveWordsRes, err error) {
|
||||
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, strings.TrimSpace(req.Code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &api.SensitiveWordsRes{TenantID: tenant.Id, TenantCode: tenant.Code, Items: []*api.SensitiveWordModel{}}
|
||||
columns := dao.ImSensitiveWords.Columns()
|
||||
groupColumns := dao.ImSensitiveWordGroups.Columns()
|
||||
var rows []struct {
|
||||
ID int64 `json:"id"`
|
||||
Word string `json:"word"`
|
||||
MatchType string `json:"match_type"`
|
||||
SceneJSON string `json:"scene_json"`
|
||||
Action string `json:"action"`
|
||||
Replacement string `json:"replacement"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
err = imtenant.RegistryDB().Model(dao.ImSensitiveWords.Table()+" w").Safe().Ctx(ctx).
|
||||
LeftJoin(dao.ImSensitiveWordGroups.Table()+" g", "g."+groupColumns.Id+"=w."+columns.GroupId).
|
||||
Fields("w."+columns.Id, "w."+columns.Word, "w."+columns.MatchType, "CAST(w."+columns.SceneJson+" AS CHAR) scene_json", "w."+columns.Action, "w."+columns.Replacement, "w."+columns.RiskLevel, "w."+columns.Priority, "w."+columns.UpdatedAt).
|
||||
Where("w."+columns.TenantId, tenant.Id).Where("w."+columns.Status, consts.StatusEnabled).WhereNull("w."+columns.DeletedAt).
|
||||
Where("g."+groupColumns.Status, consts.StatusEnabled).WhereNull("g." + groupColumns.DeletedAt).
|
||||
OrderDesc("w." + columns.Priority).OrderAsc("w." + columns.Id).Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户有效敏感词失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
var scenes []string
|
||||
_ = json.Unmarshal([]byte(row.SceneJSON), &scenes)
|
||||
res.Items = append(res.Items, &api.SensitiveWordModel{ID: row.ID, Word: row.Word, MatchType: row.MatchType, Scenes: scenes, Action: row.Action, Replacement: row.Replacement, RiskLevel: row.RiskLevel, Priority: row.Priority})
|
||||
if row.UpdatedAt != nil && row.UpdatedAt.TimestampMilli() > res.Version {
|
||||
res.Version = row.UpdatedAt.TimestampMilli()
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyActive(ctx context.Context, req *api.MessageKeyActiveReq) (res *api.MessageKeyActiveRes, err error) {
|
||||
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.MessageKeyActiveRes{ImMessageKeyModel: key}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyVersion(ctx context.Context, req *api.MessageKeyVersionReq) (res *api.MessageKeyVersionRes, err error) {
|
||||
key, err := service.SysImMessageEncryption().GetKey(ctx, &sysin.ImMessageKeyGetInp{TenantCode: req.TenantCode, Version: req.Version})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &api.MessageKeyVersionRes{ImMessageKeyModel: key}, nil
|
||||
}
|
||||
|
||||
func (c *cConfig) MessageKeyAck(ctx context.Context, req *api.MessageKeyAckReq) (res *api.MessageKeyAckRes, err error) {
|
||||
err = service.SysImMessageEncryption().Ack(ctx, &sysin.ImMessageKeyAckInp{TenantCode: req.TenantCode, Version: req.Version})
|
||||
return
|
||||
}
|
||||
|
||||
func defaultModerationPolicy(tenant *entity.ImTenant) *api.ModerationPolicyRes {
|
||||
return &api.ModerationPolicyRes{TenantID: tenant.Id, TenantCode: tenant.Code, Enabled: false,
|
||||
AliyunEnabled: false, LocalWordsEnabled: false, Scenes: cloneBoolMap(defaultModerationScenes),
|
||||
RiskActions: cloneStringMap(defaultModerationRiskActions), FailureMode: "allow",
|
||||
BlockMessage: "内容不符合平台规范,请修改后重试"}
|
||||
}
|
||||
|
||||
func defaultRealnamePolicy(tenantID int64) *api.RealnamePolicyRes {
|
||||
return &api.RealnamePolicyRes{
|
||||
TenantID: tenantID,
|
||||
RealnameReviewPolicy: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneBoolMap(src map[string]bool) map[string]bool {
|
||||
dst := make(map[string]bool, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
func cloneStringMap(src map[string]string) map[string]string {
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
func stringValue(values map[string]any, key, fallback string) string {
|
||||
if value, ok := values[key].(string); ok && strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
func intValue(values map[string]any, key string, fallback int) int {
|
||||
value := gconv.Int(values[key])
|
||||
if value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
func moderationFailureMode(value string) string {
|
||||
if strings.TrimSpace(value) == "block" {
|
||||
return "block"
|
||||
}
|
||||
return "allow"
|
||||
}
|
||||
|
||||
func (c *cConfig) TenantLoginIPWhitelist(ctx context.Context, req *api.TenantLoginIPWhitelistReq) (res *api.TenantLoginIPWhitelistRes, err error) {
|
||||
if _, err = getTenant(ctx, dao.ImTenant.Columns().Id, req.Id); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// 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.
|
||||
@@ -0,0 +1,103 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// ==========================================================================
|
||||
// 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)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func Authorize(r *ghttp.Request) bool {
|
||||
signature := strings.TrimSpace(r.Header.Get(headerSignature))
|
||||
expectedService := g.Cfg().MustGet(ctx, "imConfigApi.serviceName", "im-server").String()
|
||||
secret := configuredSecret(r)
|
||||
if serviceName == "" || timestampRaw == "" || nonce == "" || signature == "" || serviceName != expectedService || secret == "" {
|
||||
if serviceName == "" || timestampRaw == "" || nonce == "" || signature == "" || !isAllowedService(serviceName, expectedService) || secret == "" {
|
||||
writeAuthError(r, http.StatusUnauthorized, "internal signature required")
|
||||
return false
|
||||
}
|
||||
@@ -85,6 +85,19 @@ func Authorize(r *ghttp.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isAllowedService(serviceName, expectedServices string) bool {
|
||||
serviceName = strings.TrimSpace(serviceName)
|
||||
if serviceName == "" {
|
||||
return false
|
||||
}
|
||||
for _, expected := range strings.Split(expectedServices, ",") {
|
||||
if serviceName == strings.TrimSpace(expected) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func configuredSecret(r *ghttp.Request) string {
|
||||
ctx := r.GetCtx()
|
||||
secret := g.Cfg().MustGet(ctx, "imConfigApi.serviceSecret").String()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package immessagecrypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/utility/encrypt"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func GenerateDEK() ([]byte, error) {
|
||||
key := make([]byte, encrypt.AESGCMKeySize)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, gerror.Wrap(err, "生成商户消息密钥失败")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func WrapDEK(ctx context.Context, tenantID int64, version uint, dek []byte) (ciphertext, nonce []byte, err error) {
|
||||
kek, err := loadRootKEK(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return encrypt.AESGCMEncrypt(dek, kek, keyAAD(tenantID, version))
|
||||
}
|
||||
|
||||
func UnwrapDEK(ctx context.Context, tenantID int64, version uint, ciphertext, nonce []byte) ([]byte, error) {
|
||||
kek, err := loadRootKEK(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dek, err := encrypt.AESGCMDecrypt(ciphertext, nonce, kek, keyAAD(tenantID, version))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "解包商户消息密钥失败")
|
||||
}
|
||||
if len(dek) != encrypt.AESGCMKeySize {
|
||||
return nil, gerror.New("商户消息密钥长度不正确")
|
||||
}
|
||||
return dek, nil
|
||||
}
|
||||
|
||||
func EncodeDEK(dek []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(dek)
|
||||
}
|
||||
|
||||
func loadRootKEK(ctx context.Context) ([]byte, error) {
|
||||
columns := dao.SysDictData.Columns()
|
||||
value, err := dao.SysDictData.Ctx(ctx).
|
||||
Fields(columns.Value).
|
||||
Where(columns.Type, consts.ImInternalSecretDictType).
|
||||
Where(columns.Label, consts.ImMessageRootKEKLabel).
|
||||
Where(columns.Status, consts.StatusEnabled).
|
||||
Value()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取消息根密钥失败")
|
||||
}
|
||||
if value.IsEmpty() {
|
||||
return nil, gerror.New("消息根密钥未初始化")
|
||||
}
|
||||
kek, err := base64.StdEncoding.DecodeString(value.String())
|
||||
if err != nil || len(kek) != encrypt.AESGCMKeySize {
|
||||
return nil, gerror.New("消息根密钥格式不正确")
|
||||
}
|
||||
return kek, nil
|
||||
}
|
||||
|
||||
func keyAAD(tenantID int64, version uint) []byte {
|
||||
aad := make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(aad[:8], uint64(tenantID))
|
||||
binary.BigEndian.PutUint64(aad[8:], uint64(version))
|
||||
return aad
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package immoderation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
green "github.com/alibabacloud-go/green-20220302/v3/client"
|
||||
"github.com/alibabacloud-go/tea/dara"
|
||||
)
|
||||
|
||||
type AliyunConfig struct {
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
RegionID string
|
||||
Endpoint string
|
||||
Service string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type CheckResult struct {
|
||||
RequestID string
|
||||
Latency time.Duration
|
||||
}
|
||||
|
||||
func CheckAliyunText(ctx context.Context, config AliyunConfig) (*CheckResult, error) {
|
||||
if strings.TrimSpace(config.AccessKeyID) == "" || strings.TrimSpace(config.AccessKeySecret) == "" {
|
||||
return nil, errors.New("AccessKey ID或AccessKey Secret未配置")
|
||||
}
|
||||
clientConfig := &openapi.Config{AccessKeyId: dara.String(config.AccessKeyID), AccessKeySecret: dara.String(config.AccessKeySecret), RegionId: dara.String(config.RegionID), Protocol: dara.String("https")}
|
||||
if endpoint := strings.TrimSpace(config.Endpoint); endpoint != "" {
|
||||
clientConfig.Endpoint = dara.String(endpoint)
|
||||
}
|
||||
client, err := green.NewClient(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Timeout <= 0 {
|
||||
config.Timeout = 1500 * time.Millisecond
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
params, _ := json.Marshal(map[string]string{"content": "内容审核连通性测试", "dataId": "admin-config-check"})
|
||||
started := time.Now()
|
||||
resp, err := client.TextModerationPlusWithContext(ctx, &green.TextModerationPlusRequest{Service: dara.String(config.Service), ServiceParameters: dara.String(string(params))}, &dara.RuntimeOptions{})
|
||||
latency := time.Since(started)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil || resp.Body == nil || dara.Int32Value(resp.Body.Code) != 200 {
|
||||
return nil, errors.New("阿里云内容审核返回失败")
|
||||
}
|
||||
return &CheckResult{RequestID: dara.StringValue(resp.Body.RequestId), Latency: latency}, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package immoderation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/model/entity"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const (
|
||||
AliyunConfigGroup = "im_third_party"
|
||||
AliyunAccessKeyIDKey = "aliyun_access_key_id"
|
||||
AliyunAccessKeySecretKey = "aliyun_access_key_secret"
|
||||
AliyunGreenRegionIDKey = "aliyun_green_region_id"
|
||||
AliyunGreenEndpointKey = "aliyun_green_endpoint"
|
||||
DefaultAliyunGreenRegionID = "cn-shanghai"
|
||||
DefaultAliyunGreenEndpoint = "green-cip.cn-shanghai.aliyuncs.com"
|
||||
)
|
||||
|
||||
type AliyunGlobalConfig struct {
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
RegionID string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func LoadAliyunGlobalConfig(ctx context.Context) (config AliyunGlobalConfig, err error) {
|
||||
columns := dao.SysConfig.Columns()
|
||||
keys := []string{
|
||||
AliyunAccessKeyIDKey,
|
||||
AliyunAccessKeySecretKey,
|
||||
AliyunGreenRegionIDKey,
|
||||
AliyunGreenEndpointKey,
|
||||
}
|
||||
var rows []*entity.SysConfig
|
||||
if err = dao.SysConfig.Ctx(ctx).
|
||||
Fields(columns.Key, columns.Value).
|
||||
Where(columns.Group, AliyunConfigGroup).
|
||||
Where(columns.Status, 1).
|
||||
WhereIn(columns.Key, keys).
|
||||
Scan(&rows); err != nil {
|
||||
return config, gerror.Wrap(err, "获取阿里云内容审核公共配置失败")
|
||||
}
|
||||
values := make(map[string]string, len(rows))
|
||||
for _, row := range rows {
|
||||
values[row.Key] = strings.TrimSpace(row.Value)
|
||||
}
|
||||
return AliyunGlobalConfig{
|
||||
AccessKeyID: values[AliyunAccessKeyIDKey],
|
||||
AccessKeySecret: values[AliyunAccessKeySecretKey],
|
||||
RegionID: firstValue(values[AliyunGreenRegionIDKey], DefaultAliyunGreenRegionID),
|
||||
Endpoint: firstValue(values[AliyunGreenEndpointKey], DefaultAliyunGreenEndpoint),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func firstValue(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
@@ -95,7 +95,12 @@ func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
|
||||
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
||||
}
|
||||
if tenant.DbName == "" {
|
||||
return "", gerror.New("租户业务库未配置")
|
||||
// 未配置独立业务库的商户回落共享默认库
|
||||
defaultLink := defaultDbLink(ctx)
|
||||
if defaultLink == "" {
|
||||
return "", gerror.New("默认数据库连接未配置,无法回落共享业务库")
|
||||
}
|
||||
return normalizeMySQLTimeOptions(defaultLink)
|
||||
}
|
||||
|
||||
defaultLink := defaultDbLink(ctx)
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package imworkbench
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
const maxResponseBody = 10 << 20
|
||||
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
ServiceName string
|
||||
Secret string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
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:"per_page"`
|
||||
}
|
||||
|
||||
func DefaultClient(ctx context.Context) *Client {
|
||||
timeout := g.Cfg().MustGet(ctx, "imWorkbenchApi.timeout", "5s").Duration()
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
secret := firstSecret(
|
||||
g.Cfg().MustGet(ctx, "imWorkbenchApi.serviceSecret").String(),
|
||||
os.Getenv("IM_WORKBENCH_API_SECRET"),
|
||||
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"),
|
||||
)
|
||||
conf := Config{
|
||||
BaseURL: g.Cfg().MustGet(ctx, "imWorkbenchApi.baseUrl").String(),
|
||||
ServiceName: g.Cfg().MustGet(ctx, "imWorkbenchApi.serviceName", "hotgo-admin").String(),
|
||||
Secret: secret,
|
||||
Timeout: timeout,
|
||||
}
|
||||
return &Client{conf: conf, http: &http.Client{Timeout: timeout}}
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, path string, values url.Values, out any) error {
|
||||
if values != nil {
|
||||
if query := values.Encode(); query != "" {
|
||||
path += "?" + query
|
||||
}
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) Post(ctx context.Context, path string, in any, out any) error {
|
||||
body, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "序列化工作台请求失败")
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, out any) error {
|
||||
req, err := c.newRequest(ctx, method, path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "IM workbench request failed url:%s err:%+v", req.URL.String(), err)
|
||||
return gerror.New("IM工作台服务暂不可用")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return parseError(resp)
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取IM工作台响应失败")
|
||||
}
|
||||
var envelope struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(responseBody, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||
if envelope.Code != 0 {
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
return json.Unmarshal(envelope.Data, out)
|
||||
}
|
||||
return json.Unmarshal(responseBody, out)
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method string, path string, body []byte) (*http.Request, error) {
|
||||
baseURL := strings.TrimRight(c.conf.BaseURL, "/")
|
||||
if baseURL == "" {
|
||||
return nil, gerror.New("IM工作台API未配置")
|
||||
}
|
||||
if c.conf.Secret == "" {
|
||||
return nil, gerror.New("IM工作台API密钥未配置")
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建IM工作台请求失败")
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
nonce, err := randomNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyHash := sha256.Sum256(body)
|
||||
signingText := strings.Join([]string{
|
||||
method,
|
||||
req.URL.RequestURI(),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
timestamp,
|
||||
nonce,
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(c.conf.Secret))
|
||||
_, _ = mac.Write([]byte(signingText))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Name", c.conf.ServiceName)
|
||||
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-Request-ID", gctx.CtxId(ctx))
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
var envelope struct {
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &envelope)
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
|
||||
func stableError(message string, reason string) error {
|
||||
if message != "" {
|
||||
return gerror.New(message)
|
||||
}
|
||||
if reason != "" {
|
||||
return gerror.New(reason)
|
||||
}
|
||||
return gerror.New("IM工作台服务暂不可用")
|
||||
}
|
||||
|
||||
func randomNonce() (string, error) {
|
||||
var b [16]byte
|
||||
if _, err := cryptorand.Read(b[:]); err != nil {
|
||||
return "", gerror.Wrap(err, "生成IM工作台请求随机数失败")
|
||||
}
|
||||
return hex.EncodeToString(b[:]), nil
|
||||
}
|
||||
|
||||
func firstSecret(values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
|
||||
value = strings.TrimSpace(os.Getenv(strings.TrimSuffix(strings.TrimPrefix(value, "${"), "}")))
|
||||
}
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
return
|
||||
}
|
||||
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
if in.TenantId > 0 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||
@@ -508,6 +508,9 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
if err = service.AdminRole().VerifyRoleId(ctx, in.RoleId); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.normalizeTenantByRole(ctx, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证部门ID
|
||||
if err = service.AdminDept().VerifyDeptId(ctx, in.DeptId); err != nil {
|
||||
@@ -606,6 +609,50 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sAdminMember) normalizeTenantByRole(ctx context.Context, in *adminin.MemberEditInp) error {
|
||||
var role *entity.AdminRole
|
||||
if err := dao.AdminRole.Ctx(ctx).
|
||||
Fields(dao.AdminRole.Columns().Id, dao.AdminRole.Columns().Key).
|
||||
WherePri(in.RoleId).
|
||||
Scan(&role); err != nil {
|
||||
return gerror.Wrap(err, "获取角色信息失败,请稍后重试!")
|
||||
}
|
||||
if role == nil || role.Id <= 0 {
|
||||
return gerror.New("角色不存在")
|
||||
}
|
||||
switch role.Key {
|
||||
case consts.ImPlatformRoleKey:
|
||||
deptId, err := s.getCompanyDeptId(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.TenantId = 0
|
||||
in.DeptId = deptId
|
||||
case "imManage":
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("IM管理员必须选择所属商户")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAdminMember) getCompanyDeptId(ctx context.Context) (int64, error) {
|
||||
value, err := dao.AdminDept.Ctx(ctx).
|
||||
Fields(dao.AdminDept.Columns().Id).
|
||||
Where(dao.AdminDept.Columns().Type, consts.DeptTypeCompany).
|
||||
Where(dao.AdminDept.Columns().Status, consts.StatusEnabled).
|
||||
OrderAsc(dao.AdminDept.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "获取公司部门失败,请稍后重试!")
|
||||
}
|
||||
if value.IsEmpty() {
|
||||
return 0, gerror.New("请先配置公司部门")
|
||||
}
|
||||
return value.Int64(), nil
|
||||
}
|
||||
|
||||
// View 获取用户信息
|
||||
func (s *sAdminMember) View(ctx context.Context, in *adminin.MemberViewInp) (res *adminin.MemberViewModel, err error) {
|
||||
if err = s.FilterAuthModel(ctx, contexts.GetUserId(ctx)).Hook(hook.MemberInfo).WherePri(in.Id).Scan(&res); err != nil {
|
||||
@@ -912,9 +959,6 @@ func (s *sAdminMember) ClusterSyncSuperAdmin(ctx context.Context, message *gredi
|
||||
// 非超管用户只能操作自己的下级角色用户,并且需要满足自身角色的数据权限设置
|
||||
func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb.Model {
|
||||
m := dao.AdminMember.Ctx(ctx)
|
||||
if s.VerifySuperId(ctx, memberId) {
|
||||
return m
|
||||
}
|
||||
|
||||
var roleId int64
|
||||
if contexts.GetUserId(ctx) == memberId {
|
||||
@@ -929,6 +973,10 @@ func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb
|
||||
roleId = ro.Int64()
|
||||
}
|
||||
|
||||
if isAdminScopeSuper(ctx, memberId, roleId) {
|
||||
return m
|
||||
}
|
||||
|
||||
roleIds, err := service.AdminRole().GetSubRoleIds(ctx, roleId, false)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "get the subordinate role permission exception, err:%+v", err)
|
||||
|
||||
@@ -37,6 +37,35 @@ func init() {
|
||||
service.RegisterAdminRole(NewAdminRole())
|
||||
}
|
||||
|
||||
func isAdminScopeSuper(ctx context.Context, memberId, roleId int64) bool {
|
||||
if service.AdminMember().VerifySuperId(ctx, memberId) {
|
||||
return true
|
||||
}
|
||||
|
||||
if memberId == contexts.GetUserId(ctx) && contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
return true
|
||||
}
|
||||
|
||||
if roleId <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Key).WherePri(roleId).Value()
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "query admin role key failed, roleId:%d err:%+v", roleId, err)
|
||||
return false
|
||||
}
|
||||
return value.String() == consts.ImPlatformRoleKey
|
||||
}
|
||||
|
||||
func getSuperRoleId(ctx context.Context) (int64, error) {
|
||||
value, err := dao.AdminRole.Ctx(ctx).Fields(dao.AdminRole.Columns().Id).Where(dao.AdminRole.Columns().Key, consts.SuperRoleKey).Value()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value.Int64(), nil
|
||||
}
|
||||
|
||||
// Verify 验证权限
|
||||
func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
||||
var (
|
||||
@@ -64,13 +93,19 @@ func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
||||
// List 获取列表
|
||||
func (s *sAdminRole) List(ctx context.Context, in *adminin.RoleListInp) (res *adminin.RoleListModel, totalCount int, err error) {
|
||||
var (
|
||||
mod = dao.AdminRole.Ctx(ctx)
|
||||
mod = dao.AdminRole.Ctx(ctx).WhereNot(dao.AdminRole.Columns().Key, consts.SuperRoleKey)
|
||||
models []*entity.AdminRole
|
||||
pid int64 = 0
|
||||
)
|
||||
|
||||
// 非超管只获取下级角色
|
||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
||||
// 超管隐藏内置根角色,但从内置根角色的子级开始展示角色树。
|
||||
if isAdminScopeSuper(ctx, contexts.GetUserId(ctx), contexts.GetRoleId(ctx)) {
|
||||
pid, err = getSuperRoleId(ctx)
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pid = contexts.GetRoleId(ctx)
|
||||
mod = mod.WhereLike(dao.AdminRole.Columns().Tree, "%"+tree.GetIdLabel(pid)+"%")
|
||||
}
|
||||
@@ -326,7 +361,7 @@ func (s *sAdminRole) VerifyRoleId(ctx context.Context, id int64) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, service.AdminMember().VerifySuperId(ctx, mb.Id))
|
||||
ids, err := s.GetSubRoleIds(ctx, mb.RoleId, isAdminScopeSuper(ctx, mb.Id, mb.RoleId))
|
||||
if err != nil {
|
||||
err = gerror.New("验证角色信息失败!")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
type sSysCultivateContent struct{}
|
||||
|
||||
func NewSysCultivateContent() *sSysCultivateContent { return &sSysCultivateContent{} }
|
||||
func init() { service.RegisterSysCultivateContent(NewSysCultivateContent()) }
|
||||
|
||||
func (s *sSysCultivateContent) Dashboard(ctx context.Context, _ *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) {
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
out := new(sysin.CultivateDashboardModel)
|
||||
db := g.DB("cultivate")
|
||||
queries := []struct {
|
||||
target *int64
|
||||
sql string
|
||||
args []any
|
||||
}{
|
||||
{&out.PlantCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=?", []any{tenantID}},
|
||||
{&out.GrowingCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='growing'", []any{tenantID}},
|
||||
{&out.BloomCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='bloomed'", []any{tenantID}},
|
||||
{&out.HarvestedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='harvested'", []any{tenantID}},
|
||||
{&out.RemovedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='removed'", []any{tenantID}},
|
||||
{&out.Active7d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 6 DAY)", []any{tenantID}},
|
||||
{&out.Active30d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 29 DAY)", []any{tenantID}},
|
||||
{&out.PostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published'", []any{tenantID}},
|
||||
{&out.PublicPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='public'", []any{tenantID}},
|
||||
{&out.FriendsPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='friends'", []any{tenantID}},
|
||||
{&out.PrivatePostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='private'", []any{tenantID}},
|
||||
{&out.CommentCount, "SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='published'", []any{tenantID}},
|
||||
{&out.LikeCount, "SELECT COALESCE(SUM(like_count),0) FROM cv_feed_post WHERE tenant_id=?", []any{tenantID}},
|
||||
{&out.TakenDownCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='taken_down'", []any{tenantID}},
|
||||
{&out.PendingAuditCount, "SELECT (SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='reviewing')+(SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='reviewing')", []any{tenantID, tenantID}},
|
||||
{&out.ApprovedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='published'", []any{tenantID}},
|
||||
{&out.RejectedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='rejected'", []any{tenantID}},
|
||||
{&out.PendingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='submitted'", []any{tenantID}},
|
||||
{&out.ProcessingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='processing'", []any{tenantID}},
|
||||
{&out.ProcessedReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status IN ('resolved','rejected')", []any{tenantID}},
|
||||
}
|
||||
for _, q := range queries {
|
||||
value, err := db.GetValue(ctx, q.sql, q.args...)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取养成统计失败")
|
||||
}
|
||||
*q.target = value.Int64()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
if in.ObjectType == "feed_comment" {
|
||||
columns := dao.FeedComment.Columns()
|
||||
mod := dao.FeedComment.Ctx(ctx).Where(columns.TenantId, tenantID)
|
||||
if in.AuthorId != "" {
|
||||
mod = mod.Where(columns.AuthorId, in.AuthorId)
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where(columns.Status, in.Status)
|
||||
}
|
||||
if in.Keyword != "" {
|
||||
mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%")
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
var rows []*entity.FeedComment
|
||||
err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取评论审核列表失败")
|
||||
}
|
||||
list = make([]*sysin.CultivateContentListModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, &sysin.CultivateContentListModel{Id: strconv.FormatUint(row.Id, 10), ObjectType: "feed_comment", PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt})
|
||||
}
|
||||
return
|
||||
}
|
||||
columns := dao.FeedPost.Columns()
|
||||
mod := dao.FeedPost.Ctx(ctx).Where(columns.TenantId, tenantID)
|
||||
if in.AuthorId != "" {
|
||||
mod = mod.Where(columns.AuthorId, in.AuthorId)
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where(columns.Status, in.Status)
|
||||
}
|
||||
if in.Keyword != "" {
|
||||
mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%")
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
var rows []*entity.FeedPost
|
||||
err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false)
|
||||
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) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
base := &sysin.CultivateContentViewModel{Media: []*sysin.CultivateMediaModel{}, Audits: []*sysin.CultivateAuditModel{}, RelatedReports: []*sysin.CultivateReportListModel{}}
|
||||
if in.ObjectType == "feed_comment" {
|
||||
c := dao.FeedComment.Columns()
|
||||
var row entity.FeedComment
|
||||
if err := dao.FeedComment.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if row.Id == 0 {
|
||||
return nil, gerror.New("评论不存在")
|
||||
}
|
||||
base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
} else {
|
||||
c := dao.FeedPost.Columns()
|
||||
var row entity.FeedPost
|
||||
if err := dao.FeedPost.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if row.Id == 0 {
|
||||
return nil, gerror.New("动态不存在")
|
||||
}
|
||||
base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, FlowerCode: row.FlowerCode, Visibility: row.Visibility, Status: row.Status, AuditStatus: row.AuditStatus, RejectReason: row.RejectReasonCode, LikeCount: row.LikeCount, CommentCount: row.CommentCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
mc := dao.FeedMedia.Columns()
|
||||
var media []*entity.FeedMedia
|
||||
if err := dao.FeedMedia.Ctx(ctx).Where(g.Map{mc.TenantId: tenantID, mc.PostId: in.ObjectId}).OrderAsc(mc.SortNo).Scan(&media); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range media {
|
||||
base.Media = append(base.Media, &sysin.CultivateMediaModel{Id: strconv.FormatUint(m.Id, 10), FileId: m.FileId, ThumbnailFileId: m.ThumbnailFileId, MediaType: m.MediaType, AuditStatus: m.AuditStatus, SortNo: m.SortNo})
|
||||
}
|
||||
}
|
||||
ac := dao.ContentAuditLog.Columns()
|
||||
var audits []*entity.ContentAuditLog
|
||||
if err := dao.ContentAuditLog.Ctx(ctx).Where(g.Map{ac.TenantId: tenantID, ac.ObjectType: in.ObjectType, ac.ObjectId: in.ObjectId}).OrderDesc(ac.Id).Scan(&audits); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
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 {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx)
|
||||
var authorID uint64
|
||||
err := dao.FeedPost.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error {
|
||||
if in.ObjectType == "feed_comment" {
|
||||
var row entity.FeedComment
|
||||
c := dao.FeedComment.Columns()
|
||||
if e := tx.Model(dao.FeedComment.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil {
|
||||
return e
|
||||
}
|
||||
authorID = row.AuthorId
|
||||
return s.reviewComment(txCtx, tx, tenantID, operatorID, in)
|
||||
}
|
||||
var row entity.FeedPost
|
||||
c := dao.FeedPost.Columns()
|
||||
if e := tx.Model(dao.FeedPost.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil {
|
||||
return e
|
||||
}
|
||||
authorID = row.AuthorId
|
||||
return s.reviewPost(txCtx, tx, tenantID, operatorID, in)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if authorID > 0 {
|
||||
publishCultivateNotice(ctx, tenantID, authorID, fmt.Sprintf("admin.content.%s.%s.%s", in.ObjectType, in.Id, in.Action), "cultivate.content.reviewed", "内容审核结果", cultivateReviewSummary(in.Action), map[string]any{"type": in.ObjectType, "object_type": in.ObjectType, "object_id": in.Id})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &sysin.CultivateContentBatchReviewModel{Failures: make(map[string]string)}
|
||||
for _, id := range in.Ids {
|
||||
item := &sysin.CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason}
|
||||
if err := item.Filter(ctx); err != nil {
|
||||
result.FailedCount++
|
||||
result.Failures[id] = err.Error()
|
||||
continue
|
||||
}
|
||||
if err := s.Review(ctx, item); err != nil {
|
||||
result.FailedCount++
|
||||
result.Failures[id] = err.Error()
|
||||
continue
|
||||
}
|
||||
result.SuccessCount++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) reviewPost(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error {
|
||||
columns := dao.FeedPost.Columns()
|
||||
var before entity.FeedPost
|
||||
err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("动态不存在")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
data := g.Map{columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")}
|
||||
afterStatus := before.Status
|
||||
switch in.Action {
|
||||
case "approve":
|
||||
if before.Status != "reviewing" && before.Status != "rejected" {
|
||||
return gerror.New("仅待审核或已拒绝动态可审核通过")
|
||||
}
|
||||
afterStatus = "published"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.AuditStatus] = "approved"
|
||||
data[columns.RejectReasonCode] = ""
|
||||
if before.PublishedAt == nil {
|
||||
data[columns.PublishedAt] = now
|
||||
}
|
||||
case "reject":
|
||||
if before.Status != "reviewing" {
|
||||
return gerror.New("仅待审核动态可拒绝")
|
||||
}
|
||||
afterStatus = "rejected"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.AuditStatus] = "rejected"
|
||||
data[columns.RejectReasonCode] = in.Reason
|
||||
case "take_down":
|
||||
if before.Status != "published" {
|
||||
return gerror.New("仅已发布动态可下架")
|
||||
}
|
||||
afterStatus = "taken_down"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.TakenDownAt] = now
|
||||
data[columns.RejectReasonCode] = in.Reason
|
||||
case "restore":
|
||||
if before.Status != "taken_down" {
|
||||
return gerror.New("仅已下架动态可恢复")
|
||||
}
|
||||
afterStatus = "published"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.TakenDownAt] = nil
|
||||
data[columns.RejectReasonCode] = ""
|
||||
}
|
||||
if _, err = tx.Model(dao.FeedPost.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新动态审核状态失败")
|
||||
}
|
||||
if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_post", before.Id, before.Status, afterStatus, in.Action, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_post", in.Id, before, data, in.Reason, now)
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) reviewComment(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error {
|
||||
columns := dao.FeedComment.Columns()
|
||||
var before entity.FeedComment
|
||||
err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("评论不存在")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
data := g.Map{columns.UpdatedAt: now}
|
||||
afterStatus := before.Status
|
||||
counterDelta := 0
|
||||
switch in.Action {
|
||||
case "approve":
|
||||
if before.Status != "reviewing" && before.Status != "rejected" {
|
||||
return gerror.New("仅待审核或已拒绝评论可审核通过")
|
||||
}
|
||||
afterStatus = "published"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.AuditStatus] = "approved"
|
||||
data[columns.PublishedAt] = now
|
||||
counterDelta = 1
|
||||
case "reject":
|
||||
if before.Status != "reviewing" {
|
||||
return gerror.New("仅待审核评论可拒绝")
|
||||
}
|
||||
afterStatus = "rejected"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.AuditStatus] = "rejected"
|
||||
data[columns.DeleteReason] = in.Reason
|
||||
case "take_down":
|
||||
if before.Status != "published" {
|
||||
return gerror.New("仅已发布评论可下架")
|
||||
}
|
||||
afterStatus = "taken_down"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.DeleteReason] = in.Reason
|
||||
counterDelta = -1
|
||||
case "restore":
|
||||
if before.Status != "taken_down" {
|
||||
return gerror.New("仅已下架评论可恢复")
|
||||
}
|
||||
afterStatus = "published"
|
||||
data[columns.Status] = afterStatus
|
||||
data[columns.DeleteReason] = ""
|
||||
counterDelta = 1
|
||||
}
|
||||
if _, err = tx.Model(dao.FeedComment.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新评论审核状态失败")
|
||||
}
|
||||
if counterDelta != 0 {
|
||||
postColumns := dao.FeedPost.Columns()
|
||||
expression := "`comment_count` + 1"
|
||||
if counterDelta < 0 {
|
||||
expression = "GREATEST(`comment_count` - 1, 0)"
|
||||
}
|
||||
if _, err = tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw(expression), postColumns.UpdatedAt: now}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新动态评论数失败")
|
||||
}
|
||||
}
|
||||
if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_comment", before.Id, before.Status, afterStatus, in.Action, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_comment", in.Id, before, data, in.Reason, now)
|
||||
}
|
||||
|
||||
func writeContentAudit(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, objectType string, objectID uint64, before, after, result string, now time.Time) error {
|
||||
id, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
columns := dao.ContentAuditLog.Columns()
|
||||
requestID := fmt.Sprintf("manual-%s-%d-%d", objectType, objectID, id)
|
||||
_, err = tx.Model(dao.ContentAuditLog.Table()).Data(g.Map{columns.Id: id, columns.TenantId: tenantID, columns.ObjectType: objectType, columns.ObjectId: objectID, columns.AuditRequestId: requestID, columns.AuditSource: "manual", columns.StatusBefore: before, columns.StatusAfter: after, columns.ResultCode: result, columns.RiskLabels: "[]", columns.OperatorId: operatorID, columns.OccurredAt: now, columns.CreatedAt: now}).Insert()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) ReportList(ctx context.Context, in *sysin.CultivateReportListInp) (list []*sysin.CultivateReportListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
columns := dao.FeedReport.Columns()
|
||||
mod := dao.FeedReport.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx))
|
||||
if in.ObjectType != "" {
|
||||
mod = mod.Where(columns.ObjectType, in.ObjectType)
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where(columns.Status, in.Status)
|
||||
}
|
||||
if in.ReporterId != "" {
|
||||
mod = mod.Where(columns.ReporterId, in.ReporterId)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
var rows []*entity.FeedReport
|
||||
if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取举报列表失败")
|
||||
}
|
||||
list = make([]*sysin.CultivateReportListModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, reportModel(row))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func reportModel(row *entity.FeedReport) *sysin.CultivateReportListModel {
|
||||
return &sysin.CultivateReportListModel{Id: strconv.FormatUint(row.Id, 10), ReporterId: strconv.FormatUint(row.ReporterId, 10), ReportedUserId: strconv.FormatUint(row.ReportedUserId, 10), ObjectType: row.ObjectType, ObjectId: row.ObjectId, ReasonCode: row.ReasonCode, Description: row.Description, EvidenceFileIds: row.EvidenceFileIds, SourcePage: row.SourcePage, Status: row.Status, HandlerId: optionalUint(row.HandlerId), HandleResult: row.HandleResult, HandledAt: row.HandledAt, CreatedAt: row.CreatedAt}
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
c := dao.FeedReport.Columns()
|
||||
var row entity.FeedReport
|
||||
if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.Id: in.ReportId, c.TenantId: tenantID}).Scan(&row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.Id == 0 {
|
||||
return nil, gerror.New("举报记录不存在")
|
||||
}
|
||||
out := &sysin.CultivateReportViewModel{CultivateReportListModel: *reportModel(&row), History: []*sysin.CultivateReportListModel{}}
|
||||
profiles, _ := cultivateUserProfiles(ctx, tenantID, []string{out.ReporterId, out.ReportedUserId})
|
||||
if p := profiles[out.ReporterId]; p != nil {
|
||||
out.ReporterNickname = p.Nickname
|
||||
}
|
||||
if p := profiles[out.ReportedUserId]; p != nil {
|
||||
out.ReportedUserNickname = p.Nickname
|
||||
}
|
||||
var history []*entity.FeedReport
|
||||
if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.TenantId: tenantID, c.ReportedUserId: row.ReportedUserId}).OrderDesc(c.Id).Limit(20).Scan(&history); err == nil {
|
||||
for _, item := range history {
|
||||
out.History = append(out.History, reportModel(item))
|
||||
}
|
||||
}
|
||||
if row.ObjectType == "feed_post" || row.ObjectType == "feed_comment" {
|
||||
view, err := s.View(ctx, &sysin.CultivateContentViewInp{ObjectType: row.ObjectType, Id: row.ObjectId, ObjectId: mustUint(row.ObjectId)})
|
||||
if err == nil {
|
||||
out.TargetContent = &view.CultivateContentListModel
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx)
|
||||
columns := dao.FeedReport.Columns()
|
||||
var report entity.FeedReport
|
||||
if err := dao.FeedReport.Ctx(ctx).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).Scan(&report); err != nil {
|
||||
return err
|
||||
}
|
||||
if report.Id == 0 {
|
||||
return gerror.New("举报记录不存在")
|
||||
}
|
||||
if report.Status == "resolved" || report.Status == "rejected" {
|
||||
return gerror.New("举报已办结,不能重复处理")
|
||||
}
|
||||
if in.ContentAction == "take_down" {
|
||||
if err := validateReportedContentCanTakeDown(ctx, tenantID, &report); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if in.UserAction != "none" && report.ReportedUserId == 0 {
|
||||
return gerror.New("被举报用户不存在,无法执行用户处置")
|
||||
}
|
||||
if in.UserAction != "none" {
|
||||
if err := governCultivateUser(ctx, tenantID, report.ReportedUserId, in.UserAction, in.Reason); err != nil {
|
||||
return gerror.Wrap(err, "用户联动处置失败")
|
||||
}
|
||||
}
|
||||
err := dao.FeedReport.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error {
|
||||
columns := dao.FeedReport.Columns()
|
||||
var before entity.FeedReport
|
||||
err := tx.Model(dao.FeedReport.Table()).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).LockUpdate().Scan(&before)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("举报记录不存在")
|
||||
}
|
||||
if before.Status == "resolved" || before.Status == "rejected" {
|
||||
return gerror.New("举报已办结,不能重复处理")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
status := in.Action
|
||||
if status == "resolve" {
|
||||
status = "resolved"
|
||||
}
|
||||
data := g.Map{columns.Status: status, columns.HandlerId: operatorID, columns.HandleResult: in.HandleResult, columns.UpdatedAt: now}
|
||||
if status == "resolved" || status == "rejected" {
|
||||
data[columns.HandledAt] = now
|
||||
}
|
||||
if in.ContentAction == "take_down" {
|
||||
if err = takeDownReportedContent(txCtx, tx, tenantID, operatorID, &before, in.Reason, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err = tx.Model(dao.FeedReport.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新举报处理状态失败")
|
||||
}
|
||||
return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "report."+in.Action, "feed_report", in.Id, before, data, in.HandleResult, now)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Action == "resolve" || in.Action == "reject" {
|
||||
summary := "你的举报已处理,感谢你帮助维护社区环境"
|
||||
if in.Action == "reject" {
|
||||
summary = "你的举报已完成核查,暂未发现违规"
|
||||
}
|
||||
publishCultivateNotice(ctx, tenantID, report.ReporterId, "admin.report."+in.Id, "cultivate.report.handled", "举报处理结果", summary, map[string]any{"type": "report", "report_id": in.Id})
|
||||
}
|
||||
if in.ContentAction != "none" || in.UserAction != "none" {
|
||||
publishCultivateNotice(ctx, tenantID, report.ReportedUserId, "admin.report.target."+in.Id, "cultivate.governance.applied", "社区治理通知", "你的内容或账号已根据社区规则进行处置", map[string]any{"type": "report", "report_id": in.Id})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) {
|
||||
columns := dao.AdminOperationLog.Columns()
|
||||
mod := dao.AdminOperationLog.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx))
|
||||
if in.OperatorId != "" {
|
||||
mod = mod.Where(columns.OperatorId, in.OperatorId)
|
||||
}
|
||||
if in.OperationType != "" {
|
||||
mod = mod.WhereLike(columns.OperationType, "%"+strings.TrimSpace(in.OperationType)+"%")
|
||||
}
|
||||
if in.TargetType != "" {
|
||||
mod = mod.Where(columns.TargetType, in.TargetType)
|
||||
}
|
||||
if in.TargetId != "" {
|
||||
mod = mod.Where(columns.TargetId, strings.TrimSpace(in.TargetId))
|
||||
}
|
||||
if in.RequestId != "" {
|
||||
mod = mod.Where(columns.RequestId, strings.TrimSpace(in.RequestId))
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
var rows []*entity.AdminOperationLog
|
||||
if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取操作日志失败")
|
||||
}
|
||||
list = make([]*sysin.CultivateOperationLogListModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, &sysin.CultivateOperationLogListModel{Id: strconv.FormatUint(row.Id, 10), OperatorId: strconv.FormatUint(row.OperatorId, 10), OperationType: row.OperationType, TargetType: row.TargetType, TargetId: row.TargetId, RequestId: row.RequestId, BeforeSnapshot: jsonValue(row.BeforeSnapshot), AfterSnapshot: jsonValue(row.AfterSnapshot), Reason: row.Reason, CreatedAt: row.CreatedAt})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func optionalUint(value uint64) string {
|
||||
if value == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatUint(value, 10)
|
||||
}
|
||||
func jsonValue(value *gjson.Json) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type cultivateProfile struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarFileId string `json:"avatar_file_id"`
|
||||
}
|
||||
|
||||
func cultivateUserProfiles(ctx context.Context, tenantID int64, userIDs []string) (map[string]*cultivateProfile, error) {
|
||||
result := make(map[string]*cultivateProfile)
|
||||
ids := make([]int64, 0, len(userIDs))
|
||||
for _, value := range userIDs {
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err == nil && id > 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
tenant, err := resolveImAdminTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out struct {
|
||||
Items []*cultivateProfile `json:"items"`
|
||||
}
|
||||
path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/users/batch"
|
||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_ids": ids}, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range out.Items {
|
||||
result[strconv.FormatInt(item.UserId, 10)] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func publishCultivateNotice(ctx context.Context, tenantID int64, targetUserID uint64, eventID, eventType, title, summary string, action map[string]any) {
|
||||
if targetUserID == 0 {
|
||||
return
|
||||
}
|
||||
tenant, err := resolveImAdminTenant(ctx, tenantID)
|
||||
if err == nil {
|
||||
path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/events/publish"
|
||||
payload := map[string]any{"event_id": eventID, "event_type": eventType, "actor_id": contexts.GetUserId(ctx), "target_user_id": targetUserID, "title": title, "summary": summary, "action": action}
|
||||
err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil)
|
||||
}
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "养成通知投递失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func cultivateReviewSummary(action string) string {
|
||||
switch action {
|
||||
case "approve":
|
||||
return "你的内容已审核通过"
|
||||
case "reject":
|
||||
return "你的内容未通过审核,请修改后重试"
|
||||
case "take_down":
|
||||
return "你的内容已根据社区规则下架"
|
||||
case "restore":
|
||||
return "你的内容已恢复展示"
|
||||
default:
|
||||
return "你的内容审核状态已更新"
|
||||
}
|
||||
}
|
||||
|
||||
func mustUint(value string) uint64 {
|
||||
id, _ := strconv.ParseUint(value, 10, 64)
|
||||
return id
|
||||
}
|
||||
|
||||
func governCultivateUser(ctx context.Context, tenantID int64, userID uint64, action, reason string) error {
|
||||
tenant, err := resolveImAdminTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventType := consts.ImAdminCommandUserFreeze
|
||||
if action == "ban" {
|
||||
eventType = consts.ImAdminCommandUserBan
|
||||
}
|
||||
path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users/" + strconv.FormatUint(userID, 10) + "/action"
|
||||
payload := map[string]any{"event_type": eventType, "actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10), "reason": reason}
|
||||
return imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil)
|
||||
}
|
||||
|
||||
func takeDownReportedContent(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, report *entity.FeedReport, reason string, now time.Time) error {
|
||||
id := mustUint(report.ObjectId)
|
||||
if id == 0 {
|
||||
return gerror.New("举报对象ID不正确")
|
||||
}
|
||||
if report.ObjectType == "feed_post" {
|
||||
columns := dao.FeedPost.Columns()
|
||||
var before entity.FeedPost
|
||||
if err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil {
|
||||
return err
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("被举报动态不存在")
|
||||
}
|
||||
if before.Status != "published" {
|
||||
return gerror.New("仅已发布动态可联动下架")
|
||||
}
|
||||
data := g.Map{columns.Status: "taken_down", columns.TakenDownAt: now, columns.RejectReasonCode: reason, columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")}
|
||||
if _, err := tx.Model(dao.FeedPost.Table()).Where(columns.Id, id).Data(data).Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now)
|
||||
}
|
||||
if report.ObjectType == "feed_comment" {
|
||||
columns := dao.FeedComment.Columns()
|
||||
var before entity.FeedComment
|
||||
if err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil {
|
||||
return err
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("被举报评论不存在")
|
||||
}
|
||||
if before.Status != "published" {
|
||||
return gerror.New("仅已发布评论可联动下架")
|
||||
}
|
||||
data := g.Map{columns.Status: "taken_down", columns.DeleteReason: reason, columns.UpdatedAt: now}
|
||||
if _, err := tx.Model(dao.FeedComment.Table()).Where(columns.Id, id).Data(data).Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
postColumns := dao.FeedPost.Columns()
|
||||
if _, err := tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw("GREATEST(`comment_count` - 1, 0)"), postColumns.UpdatedAt: now}).Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now)
|
||||
}
|
||||
return gerror.New("该举报对象不支持内容联动下架")
|
||||
}
|
||||
|
||||
func validateReportedContentCanTakeDown(ctx context.Context, tenantID int64, report *entity.FeedReport) error {
|
||||
id := mustUint(report.ObjectId)
|
||||
if id == 0 {
|
||||
return gerror.New("举报对象ID不正确")
|
||||
}
|
||||
switch report.ObjectType {
|
||||
case "feed_post":
|
||||
columns := dao.FeedPost.Columns()
|
||||
value, err := dao.FeedPost.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value.String() != "published" {
|
||||
return gerror.New("仅已发布动态可联动下架")
|
||||
}
|
||||
case "feed_comment":
|
||||
columns := dao.FeedComment.Columns()
|
||||
value, err := dao.FeedComment.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value.String() != "published" {
|
||||
return gerror.New("仅已发布评论可联动下架")
|
||||
}
|
||||
default:
|
||||
return gerror.New("该举报对象不支持内容联动下架")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/library/location"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
type sSysCultivateFlower struct{}
|
||||
|
||||
func NewSysCultivateFlower() *sSysCultivateFlower { return &sSysCultivateFlower{} }
|
||||
func init() { service.RegisterSysCultivateFlower(NewSysCultivateFlower()) }
|
||||
|
||||
func cultivateTenantID(ctx context.Context) int64 {
|
||||
id := contexts.GetTenantId(ctx)
|
||||
if id <= 0 {
|
||||
id = g.Cfg().MustGet(ctx, "cultivate.defaultTenantId", 1).Int64()
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) {
|
||||
c := dao.FlowerCatalog.Columns()
|
||||
mod := dao.FlowerCatalog.Ctx(ctx).Fields("cv_flower_catalog.*, COUNT(cv_flower_stage_rule.id) AS stage_count").LeftJoin(dao.FlowerStageRule.Table(), "cv_flower_stage_rule.flower_catalog_id=cv_flower_catalog.id AND cv_flower_stage_rule.tenant_id=cv_flower_catalog.tenant_id").Where("cv_flower_catalog."+c.TenantId, cultivateTenantID(ctx)).Group("cv_flower_catalog." + c.Id)
|
||||
if in.FlowerCode != "" {
|
||||
mod = mod.WhereLike("cv_flower_catalog."+c.FlowerCode, "%"+strings.TrimSpace(in.FlowerCode)+"%")
|
||||
}
|
||||
if in.FlowerName != "" {
|
||||
mod = mod.WhereLike("cv_flower_catalog."+c.FlowerName, "%"+strings.TrimSpace(in.FlowerName)+"%")
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("cv_flower_catalog."+c.Status, in.Status)
|
||||
}
|
||||
if in.ConfigVersion != "" {
|
||||
mod = mod.Where("cv_flower_catalog."+c.ConfigVersion, in.ConfigVersion)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("cv_flower_catalog."+c.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("cv_flower_catalog." + c.SortNo).OrderDesc("cv_flower_catalog." + c.Id)
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取花种列表失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysCultivateFlower) View(ctx context.Context, in *sysin.CultivateFlowerViewInp) (res *sysin.CultivateFlowerViewModel, err error) {
|
||||
c := dao.FlowerCatalog.Columns()
|
||||
var catalog entity.FlowerCatalog
|
||||
if err = dao.FlowerCatalog.Ctx(ctx).Where(g.Map{c.Id: in.Id, c.TenantId: cultivateTenantID(ctx)}).Scan(&catalog); err != nil {
|
||||
return
|
||||
}
|
||||
if catalog.Id == 0 {
|
||||
return nil, gerror.New("花种不存在")
|
||||
}
|
||||
res = &sysin.CultivateFlowerViewModel{CultivateFlowerEditInp: sysin.CultivateFlowerEditInp{Id: catalog.Id, FlowerCode: catalog.FlowerCode, FlowerName: catalog.FlowerName, FlowerDesc: catalog.FlowerDesc, IconFileId: catalog.IconFileId, Status: catalog.Status, DailyWaterLimit: catalog.DailyWaterLimit, DailyFertilizeLimit: catalog.DailyFertilizeLimit, ShareConfig: catalog.ShareConfig, SortNo: catalog.SortNo}, ConfigVersion: catalog.ConfigVersion, CreatedAt: catalog.CreatedAt, UpdatedAt: catalog.UpdatedAt}
|
||||
rc := dao.FlowerStageRule.Columns()
|
||||
var rules []*entity.FlowerStageRule
|
||||
if err = dao.FlowerStageRule.Ctx(ctx).Where(g.Map{rc.TenantId: cultivateTenantID(ctx), rc.FlowerCatalogId: catalog.Id}).OrderAsc(rc.SortNo).OrderAsc(rc.Id).Scan(&rules); err != nil {
|
||||
return
|
||||
}
|
||||
for _, rule := range rules {
|
||||
actions := []string{}
|
||||
if rule.AllowedActions != nil {
|
||||
_ = json.Unmarshal(rule.AllowedActions.MustToJson(), &actions)
|
||||
}
|
||||
res.StageRules = append(res.StageRules, &sysin.CultivateStageRuleEditInp{Id: rule.Id, Stage: rule.Stage, NextStage: rule.NextStage, RequiredPoints: rule.RequiredPoints, WaterPoints: rule.WaterPoints, FertilizePoints: rule.FertilizePoints, AllowedActions: actions, ImageFileId: rule.ImageFileId, AnimationFileId: rule.AnimationFileId, ProgressTextTemplate: rule.ProgressTextTemplate, CompletionText: rule.CompletionText, SortNo: rule.SortNo})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowerEditInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
if err = validateCultivateAssets(ctx, tenantID, in); err != nil {
|
||||
return err
|
||||
}
|
||||
operatorID := contexts.GetUserId(ctx)
|
||||
now := time.Now().UTC()
|
||||
version := "cat_" + now.Format("20060102_150405")
|
||||
return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error {
|
||||
c := dao.FlowerCatalog.Columns()
|
||||
var before entity.FlowerCatalog
|
||||
if in.Id > 0 {
|
||||
if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil {
|
||||
return e
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("花种不存在")
|
||||
}
|
||||
if before.FlowerCode != in.FlowerCode {
|
||||
return gerror.New("花种编码创建后不可修改")
|
||||
}
|
||||
}
|
||||
shareJSON := "{}"
|
||||
if in.ShareConfig != nil {
|
||||
shareJSON = in.ShareConfig.MustToJsonString()
|
||||
}
|
||||
data := g.Map{c.FlowerName: in.FlowerName, c.FlowerDesc: in.FlowerDesc, c.IconFileId: in.IconFileId, c.Status: in.Status, c.ConfigVersion: version, c.DailyWaterLimit: in.DailyWaterLimit, c.DailyFertilizeLimit: in.DailyFertilizeLimit, c.ShareConfig: shareJSON, c.SortNo: in.SortNo, c.UpdatedAt: now}
|
||||
if in.Status == "online" {
|
||||
data[c.OnlineAt] = now
|
||||
data[c.OfflineAt] = nil
|
||||
} else if in.Status == "offline" {
|
||||
data[c.OfflineAt] = now
|
||||
}
|
||||
if in.Id == 0 {
|
||||
id, e := imtenant.NextSnowflakeID(txCtx)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
in.Id = uint64(id)
|
||||
data[c.Id] = id
|
||||
data[c.TenantId] = tenantID
|
||||
data[c.FlowerCode] = in.FlowerCode
|
||||
data[c.CreatedAt] = now
|
||||
if _, e = tx.Model(dao.FlowerCatalog.Table()).Data(data).Insert(); e != nil {
|
||||
return gerror.Wrap(e, "新增花种失败")
|
||||
}
|
||||
} else {
|
||||
if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).Data(data).Update(); e != nil {
|
||||
return gerror.Wrap(e, "修改花种失败")
|
||||
}
|
||||
}
|
||||
rc := dao.FlowerStageRule.Columns()
|
||||
if _, e := tx.Model(dao.FlowerStageRule.Table()).Where(g.Map{rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id}).Delete(); e != nil {
|
||||
return e
|
||||
}
|
||||
for _, rule := range in.StageRules {
|
||||
id, e := imtenant.NextSnowflakeID(txCtx)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
actions, _ := json.Marshal(rule.AllowedActions)
|
||||
_, e = tx.Model(dao.FlowerStageRule.Table()).Data(g.Map{rc.Id: id, rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id, rc.Stage: rule.Stage, rc.NextStage: rule.NextStage, rc.RequiredPoints: rule.RequiredPoints, rc.WaterPoints: rule.WaterPoints, rc.FertilizePoints: rule.FertilizePoints, rc.AllowedActions: string(actions), rc.ImageFileId: rule.ImageFileId, rc.AnimationFileId: rule.AnimationFileId, rc.ProgressTextTemplate: rule.ProgressTextTemplate, rc.CompletionText: rule.CompletionText, rc.SortNo: rule.SortNo, rc.CreatedAt: now, rc.UpdatedAt: now}).Insert()
|
||||
if e != nil {
|
||||
return gerror.Wrap(e, "保存成长阶段失败")
|
||||
}
|
||||
}
|
||||
return writeCultivateOperation(txCtx, tx, tenantID, operatorID, map[bool]string{true: "flower.edit", false: "flower.create"}[before.Id > 0], "flower", fmt.Sprint(in.Id), before, in, "", now)
|
||||
})
|
||||
}
|
||||
|
||||
func validateCultivateAssets(ctx context.Context, tenantID int64, in *sysin.CultivateFlowerEditInp) error {
|
||||
seen := make(map[string]struct{})
|
||||
fileIDs := make([]string, 0, 1+len(in.StageRules)*2)
|
||||
add := func(fileID string) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
if fileID == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[fileID]; ok {
|
||||
return
|
||||
}
|
||||
seen[fileID] = struct{}{}
|
||||
fileIDs = append(fileIDs, fileID)
|
||||
}
|
||||
add(in.IconFileId)
|
||||
for _, rule := range in.StageRules {
|
||||
add(rule.ImageFileId)
|
||||
add(rule.AnimationFileId)
|
||||
}
|
||||
if len(fileIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
tenant, err := resolveImAdminTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/files/validate"
|
||||
for offset := 0; offset < len(fileIDs); offset += 20 {
|
||||
end := offset + 20
|
||||
if end > len(fileIDs) {
|
||||
end = len(fileIDs)
|
||||
}
|
||||
var out struct {
|
||||
Items []struct {
|
||||
FileId string `json:"file_id"`
|
||||
ReviewStatus string `json:"review_status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_id": 0, "file_ids": fileIDs[offset:end]}, &out); err != nil {
|
||||
return gerror.Wrap(err, "校验花种素材失败")
|
||||
}
|
||||
for _, item := range out.Items {
|
||||
if item.ReviewStatus == "risky" {
|
||||
return gerror.Newf("素材 %s 审核未通过", item.FileId)
|
||||
}
|
||||
if item.ReviewStatus == "pending" {
|
||||
return gerror.Newf("素材 %s 尚在审核中", item.FileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error {
|
||||
tenantID := cultivateTenantID(ctx)
|
||||
operatorID := contexts.GetUserId(ctx)
|
||||
now := time.Now().UTC()
|
||||
return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error {
|
||||
c := dao.FlowerCatalog.Columns()
|
||||
var before entity.FlowerCatalog
|
||||
if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil {
|
||||
return e
|
||||
}
|
||||
if before.Id == 0 {
|
||||
return gerror.New("花种不存在")
|
||||
}
|
||||
data := g.Map{c.Status: in.Status, c.ConfigVersion: "cat_" + now.Format("20060102_150405"), c.UpdatedAt: now}
|
||||
if in.Status == "online" {
|
||||
data[c.OnlineAt] = now
|
||||
data[c.OfflineAt] = nil
|
||||
} else if in.Status == "offline" {
|
||||
data[c.OfflineAt] = now
|
||||
}
|
||||
if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(c.Id, in.Id).Data(data).Update(); e != nil {
|
||||
return e
|
||||
}
|
||||
return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "flower.status", "flower", fmt.Sprint(in.Id), before, data, in.Reason, now)
|
||||
})
|
||||
}
|
||||
|
||||
func writeCultivateOperation(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, operation, targetType, targetID string, before, after any, reason string, now time.Time) error {
|
||||
id, e := imtenant.NextSnowflakeID(ctx)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
beforeJSON, _ := json.Marshal(before)
|
||||
afterJSON, _ := json.Marshal(after)
|
||||
ipAddress, userAgent := "", ""
|
||||
if request := ghttp.RequestFromCtx(ctx); request != nil {
|
||||
ipAddress = location.GetClientIp(request)
|
||||
userAgent = request.UserAgent()
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@@ -30,11 +31,18 @@ func init() {
|
||||
|
||||
// Delete 删除
|
||||
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
|
||||
oldTypes := s.getTypesByIds(ctx, in.Id)
|
||||
if count, err := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); err != nil {
|
||||
return gerror.Wrap(err, consts.ErrorORM)
|
||||
} else if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
_, err := dao.SysDictData.Ctx(ctx).Where("id", in.Id).Delete()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
s.publishDictUpdated(ctx, oldTypes)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,11 +50,18 @@ func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp)
|
||||
func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err error) {
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
if count, queryErr := dao.SysDictData.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType).Count(); queryErr != nil {
|
||||
return gerror.Wrap(queryErr, consts.ErrorORM)
|
||||
} else if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
oldTypes := s.getTypesByIds(ctx, in.Id)
|
||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
s.publishDictUpdated(ctx, append(oldTypes, in.Type))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,18 +75,45 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
||||
if in.Type == "" {
|
||||
return gerror.Wrap(err, "类型选择无效,请检查")
|
||||
}
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
|
||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
s.publishDictUpdated(ctx, []string{in.Type})
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTypesByIds 查询指定字典数据ID的类型标识,用于变更后失效缓存
|
||||
func (s *sSysDictData) getTypesByIds(ctx context.Context, ids interface{}) []string {
|
||||
columns, err := dao.SysDictData.Ctx(ctx).Where(dao.SysDictData.Columns().Id, ids).Fields(dao.SysDictData.Columns().Type).Array()
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "查询字典类型失败 err:%+v", err)
|
||||
return nil
|
||||
}
|
||||
return g.NewVar(columns).Strings()
|
||||
}
|
||||
|
||||
// publishDictUpdated 发布字典变更事件,通知 im-server 实时失效字典配置缓存
|
||||
func (s *sSysDictData) publishDictUpdated(ctx context.Context, types []string) {
|
||||
for _, t := range types {
|
||||
t = strings.TrimSpace(t)
|
||||
if t == "" || t == consts.ImInternalSecretDictType {
|
||||
continue
|
||||
}
|
||||
if err := publishImPolicyEvent(ctx, 0, "*", consts.ImConfigEventDictionaryUpdated, t, 0); err != nil {
|
||||
g.Log().Warningf(ctx, "发布字典变更事件失败 type:%s err:%+v", t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取列表
|
||||
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
|
||||
mod := dao.SysDictData.Ctx(ctx)
|
||||
mod := dao.SysDictData.Ctx(ctx).WhereNot(dao.SysDictData.Columns().Type, consts.ImInternalSecretDictType)
|
||||
// 类型ID
|
||||
if in.TypeID > 0 {
|
||||
types, err := s.GetTypes(ctx, in.TypeID)
|
||||
@@ -151,6 +193,9 @@ func (s *sSysDictData) GetTypes(ctx context.Context, id int64) (types []string,
|
||||
|
||||
// Select 获取列表
|
||||
func (s *sSysDictData) Select(ctx context.Context, in *sysin.DataSelectInp) (list sysin.DataSelectModel, err error) {
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return nil, gerror.New("内部密钥字典禁止通过通用接口读取")
|
||||
}
|
||||
// 优先从内置字典中获取
|
||||
options, err := dict.GetOptions(ctx, in.Type)
|
||||
if err == nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func init() {
|
||||
// Tree 树
|
||||
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
|
||||
var (
|
||||
mod = dao.SysDictType.Ctx(ctx)
|
||||
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||
models []*entity.SysDictType
|
||||
)
|
||||
|
||||
@@ -55,6 +55,9 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
||||
err = gerror.New("数据不存在或已删除!")
|
||||
return
|
||||
}
|
||||
if models.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
|
||||
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
|
||||
if err != nil {
|
||||
@@ -83,6 +86,18 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
||||
|
||||
// Edit 修改/新增
|
||||
func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err error) {
|
||||
if in.Type == consts.ImInternalSecretDictType {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
if in.Id > 0 {
|
||||
count, queryErr := dao.SysDictType.Ctx(ctx).WherePri(in.Id).Where(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, consts.ErrorORM)
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||
}
|
||||
}
|
||||
if err = hgorm.IsUnique(ctx, &dao.SysDictType, g.Map{dao.SysDictType.Columns().Name: in.Name}, "名称已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -105,7 +120,7 @@ func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err
|
||||
// TreeSelect 获取类型关系树选项
|
||||
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
|
||||
var (
|
||||
mod = dao.SysDictType.Ctx(ctx)
|
||||
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||
models []*entity.SysDictType
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/service"
|
||||
@@ -18,19 +17,34 @@ import (
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type imAdminScope struct {
|
||||
TenantId int64
|
||||
OperatorId int64
|
||||
RoleKey string
|
||||
RequestId string
|
||||
}
|
||||
|
||||
const (
|
||||
imAdminOperationAuditTable = "im_admin_operation_audit"
|
||||
)
|
||||
|
||||
func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imadmin.AdminScope, err error) {
|
||||
func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imAdminScope, err error) {
|
||||
return resolveImAdminScopeWithAll(ctx, selectedTenantId, false)
|
||||
}
|
||||
|
||||
func resolveImAdminScopeWithAll(ctx context.Context, selectedTenantId int64, allowAll bool) (scope imAdminScope, err error) {
|
||||
operatorId := contexts.GetUserId(ctx)
|
||||
scope.OperatorId = operatorId
|
||||
scope.RoleKey = contexts.GetRoleKey(ctx)
|
||||
scope.RequestId = gctx.CtxId(ctx)
|
||||
if isImPlatformAdmin(ctx, operatorId) {
|
||||
if selectedTenantId <= 0 {
|
||||
if allowAll {
|
||||
return scope, nil
|
||||
}
|
||||
return scope, gerror.New("请选择IM租户")
|
||||
}
|
||||
if err = ensureImTenantActive(ctx, selectedTenantId); err != nil {
|
||||
@@ -40,7 +54,7 @@ func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope ima
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
if !contexts.IsTenantDept(ctx) {
|
||||
if !contexts.IsTenantDept(ctx) && !(contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) > 0) {
|
||||
return scope, gerror.New("当前账号无权管理IM租户")
|
||||
}
|
||||
tenantId, err := resolveImAdminBoundTenant(ctx)
|
||||
@@ -55,10 +69,13 @@ func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope ima
|
||||
}
|
||||
|
||||
func isImPlatformAdmin(ctx context.Context, operatorId int64) bool {
|
||||
if contexts.IsCompanyDept(ctx) {
|
||||
if service.AdminMember().VerifySuperId(ctx, operatorId) {
|
||||
return true
|
||||
}
|
||||
return service.AdminMember().VerifySuperId(ctx, operatorId)
|
||||
if contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||
return true
|
||||
}
|
||||
return contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) <= 0
|
||||
}
|
||||
|
||||
func resolveImAdminBoundTenant(ctx context.Context) (int64, error) {
|
||||
@@ -69,9 +86,46 @@ func resolveImAdminBoundTenant(ctx context.Context) (int64, error) {
|
||||
return tenantId, nil
|
||||
}
|
||||
|
||||
// ensureImRecordTenantAllowed 校验目标记录/租户是否在当前操作者允许范围内。
|
||||
// 平台管理员(超管/平台角色/未绑定租户的公司部门)可访问任意租户的记录;
|
||||
// 其余账号强制限定为自己绑定的租户(与 resolveImAdminScope 的范围逻辑一致)。
|
||||
// 适用于入参不含租户ID、需按已查出的记录租户做后置校验的场景。
|
||||
func ensureImRecordTenantAllowed(ctx context.Context, recordTenantId int64) error {
|
||||
if isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
return nil
|
||||
}
|
||||
if !contexts.IsTenantDept(ctx) && !(contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) > 0) {
|
||||
return gerror.New("当前账号无权管理IM租户")
|
||||
}
|
||||
boundTenantId, err := resolveImAdminBoundTenant(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if boundTenantId != recordTenantId {
|
||||
return gerror.New("无权访问其他商户的IM数据")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupImTenantByCode 按租户编码查询租户(用于入参只有租户编码的管理端接口)
|
||||
func lookupImTenantByCode(ctx context.Context, code string) (tenant *entity.ImTenant, err error) {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return nil, gerror.New("请选择IM租户")
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Code, code).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
return nil, gerror.New("IM租户不存在")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err = imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
|
||||
@@ -5,15 +5,25 @@ import (
|
||||
"testing"
|
||||
|
||||
"hotgo/internal/consts"
|
||||
_ "hotgo/internal/logic/admin"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func TestCompanyDepartmentIsImPlatformAdmin(t *testing.T) {
|
||||
func TestBoundCompanyDepartmentUsesBoundTenant(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), consts.ContextHTTPKey, &model.Context{
|
||||
User: &model.Identity{Id: 14, TenantId: 1, DeptType: consts.DeptTypeCompany, RoleKey: "imManage"},
|
||||
})
|
||||
if isImPlatformAdmin(ctx, 14) {
|
||||
t.Fatal("expected bound company department IM manager to use its bound tenant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnboundCompanyDepartmentIsImPlatformAdmin(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), consts.ContextHTTPKey, &model.Context{
|
||||
User: &model.Identity{Id: 14, DeptType: consts.DeptTypeCompany, RoleKey: "imManage"},
|
||||
})
|
||||
if !isImPlatformAdmin(ctx, 14) {
|
||||
t.Fatal("expected company department IM manager to use platform tenant selection")
|
||||
t.Fatal("expected unbound company department IM manager to use platform tenant selection")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +41,14 @@ func TestResolveImAdminBoundTenant(t *testing.T) {
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "company department with tenant binding",
|
||||
identity: &model.Identity{
|
||||
TenantId: 1,
|
||||
DeptType: consts.DeptTypeCompany,
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,18 +10,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sSysImDeadLetter struct{}
|
||||
@@ -39,7 +34,7 @@ func (s *sSysImDeadLetter) List(ctx context.Context, in *sysin.ImDeadLetterListI
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,7 +76,7 @@ func (s *sSysImDeadLetter) View(ctx context.Context, in *sysin.ImDeadLetterViewI
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +115,7 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -162,22 +157,6 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImDeadLetter) getTenant(ctx context.Context, tenantId int64) (tenant *entity.ImTenant, err error) {
|
||||
if tenantId <= 0 {
|
||||
return nil, gerror.New("请选择IM租户")
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
return nil, gerror.New("IM租户不存在")
|
||||
}
|
||||
if tenant.State != consts.ImTenantStateActive {
|
||||
return nil, gerror.New("IM租户未启用")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func convertDeadLetterModel(in *sysin.ImDeadLetterListModelRaw) *sysin.ImDeadLetterListModel {
|
||||
if in == nil {
|
||||
return &sysin.ImDeadLetterListModel{}
|
||||
|
||||
@@ -5,16 +5,22 @@ import (
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/immoderation"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sSysImExternalConfig struct{}
|
||||
@@ -36,7 +42,13 @@ func (s *sSysImExternalConfig) List(ctx context.Context, in *sysin.ImExternalCon
|
||||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||
return
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||||
return nil, 0, gerror.New("第一阶段仅默认商户可配置内容审核服务")
|
||||
}
|
||||
mod := s.model(ctx).Where("tenant_id", tenant.Id)
|
||||
if tenant.Code != "default" {
|
||||
mod = mod.WhereNot("service", consts.ImExternalServiceModeration)
|
||||
}
|
||||
if in.Service != "" {
|
||||
mod = mod.Where("service", in.Service)
|
||||
}
|
||||
@@ -68,6 +80,9 @@ func (s *sSysImExternalConfig) View(ctx context.Context, in *sysin.ImExternalCon
|
||||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||||
return nil, gerror.New("第一阶段仅默认商户可查看内容审核服务")
|
||||
}
|
||||
if err = s.model(ctx).Where("tenant_id", tenant.Id).Where("service", in.Service).Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM外部服务配置详情失败,请稍后重试!")
|
||||
}
|
||||
@@ -91,6 +106,34 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC
|
||||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||||
return gerror.New("第一阶段仅默认商户可配置内容审核服务")
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("只有平台管理员可以维护内容审核服务")
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration {
|
||||
if in.Provider != "aliyun_green" {
|
||||
return gerror.New("内容审核服务商必须为aliyun_green")
|
||||
}
|
||||
in.Endpoint = ""
|
||||
if in.ConfigJson == nil {
|
||||
in.ConfigJson = gjson.New(map[string]any{"nickname_service": "nickname_detection_pro", "chat_service": "chat_detection_pro", "timeout_ms": 1500, "failure_mode": "allow"})
|
||||
}
|
||||
config := in.ConfigJson.Map()
|
||||
if timeout := gconv.Int(config["timeout_ms"]); timeout < 200 || timeout > 10000 {
|
||||
return gerror.New("内容审核超时必须为200至10000毫秒")
|
||||
}
|
||||
if mode := gconv.String(config["failure_mode"]); mode != "allow" && mode != "block" {
|
||||
return gerror.New("内容审核异常策略不合法")
|
||||
}
|
||||
in.ConfigJson = gjson.New(map[string]any{
|
||||
"nickname_service": firstExternalValue(gconv.String(config["nickname_service"]), "nickname_detection_pro"),
|
||||
"chat_service": firstExternalValue(gconv.String(config["chat_service"]), "chat_detection_pro"),
|
||||
"timeout_ms": gconv.Int(config["timeout_ms"]),
|
||||
"failure_mode": gconv.String(config["failure_mode"]),
|
||||
})
|
||||
}
|
||||
configJson := "{}"
|
||||
if in.ConfigJson != nil && in.ConfigJson.String() != "" && in.ConfigJson.String() != "null" {
|
||||
configJson = in.ConfigJson.String()
|
||||
@@ -106,6 +149,9 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC
|
||||
if in.SecretJson != nil && in.SecretJson.String() != "" && in.SecretJson.String() != "null" && in.SecretJson.String() != "{}" {
|
||||
data["secret_json"] = in.SecretJson.String()
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration {
|
||||
data["secret_json"] = "{}"
|
||||
}
|
||||
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("service", in.Service).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新IM外部服务配置失败,请稍后重试!")
|
||||
@@ -113,6 +159,9 @@ func (s *sSysImExternalConfig) Update(ctx context.Context, in *sysin.ImExternalC
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_update", "im_external_config", 0, "success", g.Map{
|
||||
"service": in.Service, "provider": in.Provider, "enabled": in.Enabled,
|
||||
})
|
||||
if in.Service == consts.ImExternalServiceModeration {
|
||||
_ = publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "third_party.updated", "moderation", 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -127,6 +176,12 @@ func (s *sSysImExternalConfig) Check(ctx context.Context, in *sysin.ImExternalCo
|
||||
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||||
return gerror.New("第一阶段仅默认商户可检查内容审核服务")
|
||||
}
|
||||
if in.Service == consts.ImExternalServiceModeration && !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("只有平台管理员可以检查内容审核服务")
|
||||
}
|
||||
var row gdb.Record
|
||||
if row, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("service", in.Service).One(); err != nil {
|
||||
@@ -137,6 +192,8 @@ func (s *sSysImExternalConfig) Check(ctx context.Context, in *sysin.ImExternalCo
|
||||
if row == nil || row["enabled"].Int() != 1 || row["provider"].String() == "" {
|
||||
status = consts.ImExternalConfigStatusUnconfigured
|
||||
result = "未启用或服务商为空"
|
||||
} else if in.Service == consts.ImExternalServiceModeration {
|
||||
status, result = s.checkModeration(ctx, row)
|
||||
}
|
||||
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("service", in.Service).
|
||||
@@ -150,6 +207,45 @@ func (s *sSysImExternalConfig) Check(ctx context.Context, in *sysin.ImExternalCo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImExternalConfig) checkModeration(ctx context.Context, row gdb.Record) (string, string) {
|
||||
if row["provider"].String() != "aliyun_green" {
|
||||
return consts.ImExternalConfigStatusUnconfigured, "内容审核服务商必须为aliyun_green"
|
||||
}
|
||||
config := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(row["config_json"].String()), &config)
|
||||
globalConfig, err := immoderation.LoadAliyunGlobalConfig(ctx)
|
||||
if err != nil {
|
||||
return consts.ImExternalConfigStatusUnconfigured, sanitizeExternalError(err)
|
||||
}
|
||||
timeoutMS := gconv.Int(config["timeout_ms"])
|
||||
if timeoutMS <= 0 {
|
||||
timeoutMS = 1500
|
||||
}
|
||||
check, err := immoderation.CheckAliyunText(ctx, immoderation.AliyunConfig{
|
||||
AccessKeyID: globalConfig.AccessKeyID, AccessKeySecret: globalConfig.AccessKeySecret,
|
||||
RegionID: globalConfig.RegionID, Endpoint: globalConfig.Endpoint,
|
||||
Service: firstExternalValue(gconv.String(config["nickname_service"]), "nickname_detection_pro"), Timeout: time.Duration(timeoutMS) * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
return consts.ImExternalConfigStatusUnconfigured, "连通性检查失败:" + sanitizeExternalError(err)
|
||||
}
|
||||
return consts.ImExternalConfigStatusConfigured, fmt.Sprintf("检查成功,耗时%dms,RequestID=%s", check.Latency.Milliseconds(), check.RequestID)
|
||||
}
|
||||
|
||||
func firstExternalValue(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
func sanitizeExternalError(err error) string {
|
||||
value := strings.ReplaceAll(err.Error(), "\n", " ")
|
||||
if len([]rune(value)) > 120 {
|
||||
value = string([]rune(value)[:120])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *sSysImExternalConfig) model(ctx context.Context) *gdb.Model {
|
||||
return imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).Fields(
|
||||
"tenant_id",
|
||||
|
||||
@@ -2,17 +2,13 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/imworkbench"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const imFeedbackConfigTable = "im_feedback_config"
|
||||
|
||||
type sSysImFeedbackConfig struct{}
|
||||
|
||||
func NewSysImFeedbackConfig() *sSysImFeedbackConfig { return &sSysImFeedbackConfig{} }
|
||||
@@ -25,16 +21,14 @@ func (s *sSysImFeedbackConfig) Get(ctx context.Context, in *sysin.ImFeedbackConf
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.ensureDefault(ctx, db); err != nil {
|
||||
res = &sysin.ImFeedbackConfigModel{}
|
||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/feedback-config", nil, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.model(ctx, db).OrderAsc("id").Limit(1).Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM意见反馈配置失败,请稍后重试!")
|
||||
}
|
||||
if res != nil {
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
@@ -46,13 +40,10 @@ func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackC
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.ensureDefault(ctx, db); err != nil {
|
||||
return err
|
||||
}
|
||||
categories := `["bug","suggestion","complaint","other"]`
|
||||
if in.Categories != nil && in.Categories.String() != "" && in.Categories.String() != "null" {
|
||||
categories = in.Categories.String()
|
||||
@@ -61,52 +52,15 @@ func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackC
|
||||
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||
attachmentPolicy = in.AttachmentPolicy.String()
|
||||
}
|
||||
if _, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{
|
||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/feedback-config", g.Map{
|
||||
"categories": categories,
|
||||
"max_length": in.MaxLength,
|
||||
"attachment_policy": attachmentPolicy,
|
||||
"allow_contact": in.AllowContact,
|
||||
"diagnostics_notice": in.DiagnosticsNotice,
|
||||
"updated_at": gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新IM意见反馈配置失败,请稍后重试!")
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImFeedbackConfig) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Fields(
|
||||
"id",
|
||||
"CAST(categories AS CHAR) categories",
|
||||
"max_length",
|
||||
"CAST(attachment_policy AS CHAR) attachment_policy",
|
||||
"allow_contact",
|
||||
"diagnostics_notice",
|
||||
"updated_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImFeedbackConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) {
|
||||
var count int
|
||||
if count, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil {
|
||||
return gerror.Wrap(err, "检查IM意见反馈默认配置失败,请稍后重试!")
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"id": 1,
|
||||
"categories": `["bug","suggestion","complaint","other"]`,
|
||||
"max_length": 1000,
|
||||
"attachment_policy": `{"max_files":5,"purpose":"feedback"}`,
|
||||
"allow_contact": 1,
|
||||
"diagnostics_notice": "仅在你同意时上传脱敏诊断信息。",
|
||||
"created_at": gtime.Now(),
|
||||
"updated_at": gtime.Now(),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "初始化IM意见反馈默认配置失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp)
|
||||
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
|
||||
var item imInviteCodeResponse
|
||||
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
|
||||
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key)
|
||||
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key, 0, "")
|
||||
if createErr != nil {
|
||||
err = createErr
|
||||
} else if len(codes) > 0 {
|
||||
@@ -150,15 +150,27 @@ func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var boundUserLabel string
|
||||
if in.UserId > 0 {
|
||||
tenant, terr := resolveImAdminTenant(ctx, scope.TenantId)
|
||||
if terr != nil {
|
||||
return nil, terr
|
||||
}
|
||||
label, lerr := inviteBoundUserLabel(ctx, tenant, in.UserId)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
boundUserLabel = label
|
||||
}
|
||||
key := strings.TrimSpace(in.IdempotencyKey)
|
||||
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key)
|
||||
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key, in.UserId, boundUserLabel)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
} else {
|
||||
res = convertInviteBatch(batch)
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark, "boundUserId": in.UserId})
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -358,7 +370,7 @@ func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int
|
||||
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
|
||||
}
|
||||
|
||||
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
|
||||
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string, boundUserID int64, boundUserLabel string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
|
||||
if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
|
||||
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
||||
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
||||
@@ -402,11 +414,16 @@ func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int6
|
||||
}
|
||||
for i := range codes {
|
||||
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
|
||||
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
|
||||
VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
|
||||
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at,bound_user_id,bound_user_label)
|
||||
VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now, boundUserID, boundUserLabel); txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now
|
||||
if boundUserID > 0 {
|
||||
id := boundUserID
|
||||
codes[i].BoundUserID = &id
|
||||
codes[i].BoundUserLabel = boundUserLabel
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -33,7 +33,12 @@ func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandL
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
where, args := buildImKafkaCommandWhere(in)
|
||||
// 强制租户范围:商户绑定账号忽略前端所选租户,平台管理员使用所选租户
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
where, args := buildImKafkaCommandWhere(in, scope.TenantId)
|
||||
db := imtenant.RegistryDB()
|
||||
totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...)
|
||||
if err != nil {
|
||||
@@ -91,16 +96,16 @@ func (s *sSysImKafkaCommand) View(ctx context.Context, in *sysin.ImKafkaCommandV
|
||||
if err = row.Struct(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析IM后台命令详情失败,请稍后重试!")
|
||||
}
|
||||
// 后置校验记录归属租户是否在操作者允许范围内
|
||||
if err = ensureImRecordTenantAllowed(ctx, res.TenantId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp) (string, []any) {
|
||||
parts := []string{"1=1"}
|
||||
args := []any{}
|
||||
if in.TenantId > 0 {
|
||||
parts = append(parts, "tenant_id=?")
|
||||
args = append(args, in.TenantId)
|
||||
}
|
||||
func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp, scopeTenantId int64) (string, []any) {
|
||||
parts := []string{"tenant_id=?"}
|
||||
args := []any{scopeTenantId}
|
||||
if in.CommandId != "" {
|
||||
parts = append(parts, "command_id LIKE ?")
|
||||
args = append(args, "%"+in.CommandId+"%")
|
||||
@@ -136,6 +141,14 @@ func (s *sSysImKafkaCommand) CacheInvalidate(ctx context.Context, in *sysin.ImKa
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 校验目标租户是否在操作者允许范围内
|
||||
tenant, err := lookupImTenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = ensureImRecordTenantAllowed(ctx, tenant.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actor := &imkafka.Actor{
|
||||
Type: "admin",
|
||||
@@ -170,6 +183,14 @@ func (s *sSysImKafkaCommand) WorkbenchEvent(ctx context.Context, in *sysin.ImKaf
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 校验目标租户是否在操作者允许范围内
|
||||
tenant, err := lookupImTenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = ensureImRecordTenantAllowed(ctx, tenant.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actor := &imkafka.Actor{
|
||||
Type: "admin",
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
@@ -128,6 +130,26 @@ func (s *sSysImLoginFailureLog) AddBlacklist(ctx context.Context, in *sysin.ImLo
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImLoginFailureLog) Unlock(ctx context.Context, in *sysin.ImLoginFailureUnlockInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/login-lockouts/release"
|
||||
var out struct {
|
||||
AccountHash string `json:"account_hash"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]string{"account_hash": in.AccountHash}, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Infof(ctx, "登录锁定已解锁 tenantId:%d accountHash:%s adminId:%d status:%s", tenant.Id, in.AccountHash, contexts.GetUserId(ctx), out.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImLoginFailureLog) BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (*sysin.ImUserCommandModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,17 +2,13 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/imworkbench"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const imMailboxConfigTable = "im_mailbox_config"
|
||||
|
||||
type sSysImMailboxConfig struct{}
|
||||
|
||||
func NewSysImMailboxConfig() *sSysImMailboxConfig { return &sSysImMailboxConfig{} }
|
||||
@@ -25,16 +21,14 @@ func (s *sSysImMailboxConfig) Get(ctx context.Context, in *sysin.ImMailboxConfig
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.ensureDefault(ctx, db); err != nil {
|
||||
res = &sysin.ImMailboxConfigModel{}
|
||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/mailbox-config", nil, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.model(ctx, db).Where("id", 1).Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM总经理信箱配置失败,请稍后重试!")
|
||||
}
|
||||
if res != nil {
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
@@ -46,13 +40,10 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.ensureDefault(ctx, db); err != nil {
|
||||
return err
|
||||
}
|
||||
requiredFields := `["subject","content"]`
|
||||
if in.RequiredFields != nil && in.RequiredFields.String() != "" && in.RequiredFields.String() != "null" {
|
||||
requiredFields = in.RequiredFields.String()
|
||||
@@ -61,7 +52,7 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
||||
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||
attachmentPolicy = in.AttachmentPolicy.String()
|
||||
}
|
||||
if _, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{
|
||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/mailbox-config", g.Map{
|
||||
"enabled": in.Enabled,
|
||||
"allow_anonymous": in.AllowAnonymous,
|
||||
"required_fields": requiredFields,
|
||||
@@ -69,53 +60,9 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
||||
"attachment_policy": attachmentPolicy,
|
||||
"notice_title": in.NoticeTitle,
|
||||
"notice_content": in.NoticeContent,
|
||||
"notice_updated_at": gtime.Now(),
|
||||
"updated_at": gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新IM总经理信箱配置失败,请稍后重试!")
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImMailboxConfig) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Fields(
|
||||
"id",
|
||||
"enabled",
|
||||
"allow_anonymous",
|
||||
"CAST(required_fields AS CHAR) required_fields",
|
||||
"max_length",
|
||||
"CAST(attachment_policy AS CHAR) attachment_policy",
|
||||
"notice_title",
|
||||
"notice_content",
|
||||
"notice_updated_at",
|
||||
"updated_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImMailboxConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) {
|
||||
var count int
|
||||
if count, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil {
|
||||
return gerror.Wrap(err, "检查IM总经理信箱默认配置失败,请稍后重试!")
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"id": 1,
|
||||
"enabled": 1,
|
||||
"allow_anonymous": 1,
|
||||
"required_fields": `["subject","content"]`,
|
||||
"max_length": 5000,
|
||||
"attachment_policy": `{"max_files":5,"purpose":"executive_mailbox"}`,
|
||||
"notice_title": "总经理信箱",
|
||||
"notice_content": "请如实填写诉求,匿名提交将按企业政策保护展示身份。",
|
||||
"notice_updated_at": gtime.Now(),
|
||||
"created_at": gtime.Now(),
|
||||
"updated_at": gtime.Now(),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "初始化IM总经理信箱默认配置失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/immessagecrypto"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
imMessageEncryptionKeyTable = "im_message_encryption_keys"
|
||||
imTenantMessageEncryptionColumn = "message_encryption_enabled"
|
||||
)
|
||||
|
||||
type sSysImMessageEncryption struct{}
|
||||
|
||||
func NewSysImMessageEncryption() *sSysImMessageEncryption { return &sSysImMessageEncryption{} }
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImMessageEncryption(NewSysImMessageEncryption())
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.requireSuperAdmin(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
db := imtenant.RegistryDB()
|
||||
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
|
||||
LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id").
|
||||
Fields(
|
||||
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
|
||||
"COALESCE(c."+imTenantMessageEncryptionColumn+",0) 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='pending'),0) pending_version",
|
||||
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
|
||||
"(SELECT MAX(COALESCE(k.activated_at,k.created_at)) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) last_rotated_at",
|
||||
)
|
||||
if in.TenantId > 0 {
|
||||
mod = mod.Where("t.id", in.TenantId)
|
||||
}
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
if in.Enabled != nil {
|
||||
mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled)
|
||||
}
|
||||
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dek, err := immessagecrypto.GenerateDEK()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ciphertext, nonce, err := immessagecrypto.WrapDEK(ctx, tenant.Id, 1, dek)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
count, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("该商户已经初始化消息密钥")
|
||||
}
|
||||
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"tenant_id": tenant.Id, "key_version": 1, "algorithm": consts.ImMessageKeyAlgorithm,
|
||||
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusActive,
|
||||
"created_at": gtime.Now(), "activated_at": gtime.Now(), "created_by": contexts.GetUserId(ctx),
|
||||
"remark": strings.TrimSpace(in.Remark),
|
||||
}).Insert()
|
||||
if insertErr != nil {
|
||||
return gerror.Wrap(insertErr, "初始化商户消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_initialize", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": 1})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", 1)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var version uint
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
pending, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusPending).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查待激活消息密钥失败")
|
||||
}
|
||||
if pending > 0 {
|
||||
return gerror.New("该商户已有待激活密钥,请等待IM Server确认")
|
||||
}
|
||||
value, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Fields("COALESCE(MAX(key_version),0)").Where("tenant_id", tenant.Id).Value()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "获取消息密钥版本失败")
|
||||
}
|
||||
if value.Uint() == 0 {
|
||||
return gerror.New("请先初始化商户消息密钥")
|
||||
}
|
||||
version = value.Uint() + 1
|
||||
dek, generateErr := immessagecrypto.GenerateDEK()
|
||||
if generateErr != nil {
|
||||
return generateErr
|
||||
}
|
||||
ciphertext, nonce, wrapErr := immessagecrypto.WrapDEK(ctx, tenant.Id, version, dek)
|
||||
if wrapErr != nil {
|
||||
return wrapErr
|
||||
}
|
||||
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
||||
"tenant_id": tenant.Id, "key_version": version, "algorithm": consts.ImMessageKeyAlgorithm,
|
||||
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusPending,
|
||||
"created_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark),
|
||||
}).Insert()
|
||||
if insertErr != nil {
|
||||
return gerror.Wrap(insertErr, "创建待激活消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_rotate", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": version, "status": consts.ImMessageKeyStatusPending})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", version)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.requireSuperAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenant, err := s.tenantByID(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Enabled == 1 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("启用消息加密前必须先初始化有效密钥")
|
||||
}
|
||||
}
|
||||
result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).
|
||||
Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新商户消息加密开关失败")
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||
return gerror.New("商户配置不存在")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled})
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error) {
|
||||
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
||||
Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
|
||||
}
|
||||
mod := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id)
|
||||
if in.Version > 0 {
|
||||
mod = mod.Where("key_version", in.Version).WhereIn("status", []string{consts.ImMessageKeyStatusPending, consts.ImMessageKeyStatusActive, consts.ImMessageKeyStatusRetired})
|
||||
} else {
|
||||
mod = mod.Where("status", consts.ImMessageKeyStatusActive).OrderDesc("key_version")
|
||||
}
|
||||
row, err := mod.One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户消息密钥失败")
|
||||
}
|
||||
if row.IsEmpty() {
|
||||
return nil, gerror.New("商户消息密钥不存在")
|
||||
}
|
||||
version := row["key_version"].Uint()
|
||||
dek, err := immessagecrypto.UnwrapDEK(ctx, tenant.Id, version, row["encrypted_dek"].Bytes(), row["dek_nonce"].Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImMessageKeyModel{
|
||||
TenantId: tenant.Id, TenantCode: tenant.Code, Enabled: enabledValue.Int() == 1,
|
||||
Version: version, Algorithm: row["algorithm"].String(), Status: row["status"].String(), Key: immessagecrypto.EncodeDEK(dek),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error {
|
||||
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Version == 0 {
|
||||
return gerror.New("消息密钥版本不能为空")
|
||||
}
|
||||
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
row, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("key_version", in.Version).One()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "获取待激活消息密钥失败")
|
||||
}
|
||||
if row.IsEmpty() || row["status"].String() != consts.ImMessageKeyStatusPending {
|
||||
return gerror.New("待激活消息密钥不存在")
|
||||
}
|
||||
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).
|
||||
Data(g.Map{"status": consts.ImMessageKeyStatusRetired, "retired_at": gtime.Now()}).Update(); updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "退役旧消息密钥失败")
|
||||
}
|
||||
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenant.Id).Where("key_version", in.Version).
|
||||
Data(g.Map{"status": consts.ImMessageKeyStatusActive, "activated_at": gtime.Now()}).Update(); updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "激活新消息密钥失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.activated", "message_encryption", in.Version)
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) requireSuperAdmin(ctx context.Context) error {
|
||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("只有最高权限平台管理员可以管理消息密钥")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) tenantByID(ctx context.Context, tenantID int64) (*entity.ImTenant, error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantID).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id == 0 {
|
||||
return nil, gerror.New("商户不存在")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func (s *sSysImMessageEncryption) tenantByCode(ctx context.Context, code string) (*entity.ImTenant, error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Code, strings.TrimSpace(code)).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取商户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id == 0 {
|
||||
return nil, gerror.New("商户不存在")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId)
|
||||
tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -123,7 +123,11 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
||||
}
|
||||
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: policyJSON(in.SensitiveWordPolicy, "{}"), 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}
|
||||
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)
|
||||
}
|
||||
if in.Id > 0 {
|
||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
@@ -158,6 +162,20 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func normalizedModerationPolicy(value *gjson.Json, _ string) string {
|
||||
policy := map[string]any{
|
||||
"scenes": map[string]bool{"user_nickname": true, "user_signature": true, "group_name": true, "group_nickname": true, "private_message": true, "group_message": true, "group_announcement": true},
|
||||
"risk_actions": map[string]string{"high": "block", "medium": "block", "low": "audit_only", "none": "allow"},
|
||||
"failure_mode": "allow", "block_message": "内容不符合平台规范,请修改后重试",
|
||||
}
|
||||
if value != nil && value.String() != "" && value.String() != "null" {
|
||||
for key, item := range value.Map() {
|
||||
policy[key] = item
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -238,6 +256,90 @@ func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImS
|
||||
return id, err
|
||||
}
|
||||
|
||||
// GetLoginLockoutConfig 读取租户已发布策略中的登录失败锁定配置,无配置时返回默认值。
|
||||
func (s *sSysImSecurityPolicy) GetLoginLockoutConfig(ctx context.Context, in *sysin.ImLoginLockoutConfigGetInp) (res *sysin.ImLoginLockoutConfigModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &sysin.ImLoginLockoutConfigModel{Enabled: false, PasswordMaxAttempts: 5, LockMinutes: 1440, WindowMinutes: 60}
|
||||
c := dao.ImSecurityPolicy.Columns()
|
||||
var row gdb.Record
|
||||
if row, err = imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(c.Id, c.PolicyVersion, "CAST("+c.LoginRateLimit+" AS CHAR) login_rate_limit").
|
||||
Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(c.DeletedAt).OrderDesc(c.PolicyVersion).One(); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取登录失败策略配置失败")
|
||||
}
|
||||
if row.IsEmpty() {
|
||||
return res, nil
|
||||
}
|
||||
res.PolicyId = row[c.Id].Int64()
|
||||
res.PolicyVersion = row[c.PolicyVersion].Uint()
|
||||
policy := gjson.New(row["login_rate_limit"].String())
|
||||
res.Enabled = policy.Get("enabled").Bool()
|
||||
if v := policy.Get("password_max_attempts").Int(); v >= 1 {
|
||||
res.PasswordMaxAttempts = int(v)
|
||||
}
|
||||
if v := policy.Get("lock_seconds").Int(); v >= 60 {
|
||||
res.LockMinutes = int(v / 60)
|
||||
}
|
||||
if v := policy.Get("window_seconds").Int(); v >= 60 {
|
||||
res.WindowMinutes = int(v / 60)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SaveLoginLockoutConfig 保存租户登录失败锁定配置:直接更新已发布策略,保存后通过策略变更事件即时生效。
|
||||
func (s *sSysImSecurityPolicy) SaveLoginLockoutConfig(ctx context.Context, in *sysin.ImLoginLockoutConfigSaveInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := map[string]any{"enabled": false}
|
||||
if in.Enabled {
|
||||
value = map[string]any{"enabled": true, "password_max_attempts": in.PasswordMaxAttempts, "lock_seconds": in.LockMinutes * 60, "window_seconds": in.WindowMinutes * 60}
|
||||
}
|
||||
loginRateLimit := gjson.New(value).String()
|
||||
c := dao.ImSecurityPolicy.Columns()
|
||||
db := imtenant.RegistryDB()
|
||||
var row gdb.Record
|
||||
if row, err = db.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(c.Id, c.PolicyVersion).
|
||||
Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(c.DeletedAt).OrderDesc(c.PolicyVersion).One(); err != nil {
|
||||
return gerror.Wrap(err, "获取登录失败策略配置失败")
|
||||
}
|
||||
operator := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
if !row.IsEmpty() {
|
||||
policyID := row[c.Id].Int64()
|
||||
version := row[c.PolicyVersion].Uint()
|
||||
if _, err = db.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, policyID).
|
||||
Data(g.Map{c.LoginRateLimit: loginRateLimit, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); err != nil {
|
||||
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})
|
||||
return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version)
|
||||
}
|
||||
// 租户尚无已发布策略:创建默认策略草稿并直接发布(Publish 内部已发送策略变更事件)
|
||||
id, saveErr := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "默认安全策略", LoginRateLimit: gjson.New(value), GlobalFriendEnabled: 1, MessageCurfewEnabled: 0})
|
||||
if saveErr != nil {
|
||||
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})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImSecurityPolicy) IPRuleList(ctx context.Context, in *sysin.ImSecurityIPRuleListInp) (list []*sysin.ImSecurityIPRuleModel, total int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
|
||||
@@ -15,8 +15,10 @@ import (
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/adminin"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/tree"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@@ -25,6 +27,28 @@ import (
|
||||
|
||||
type sSysImTenant struct{}
|
||||
|
||||
const (
|
||||
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
|
||||
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
|
||||
tenantConfigMessageEncryptionEnabled = "message_encryption_enabled"
|
||||
tenantConfigRealnameReviewPolicy = "realname_review_policy"
|
||||
)
|
||||
|
||||
// imManageRoleKey 商户级IM管理员角色标识
|
||||
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 {
|
||||
return &sSysImTenant{}
|
||||
}
|
||||
@@ -61,6 +85,14 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
||||
dao.ImTenant.Columns().UpdatedAt,
|
||||
)
|
||||
|
||||
if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
if tenantId <= 0 {
|
||||
err = gerror.New("当前管理员未关联IM租户")
|
||||
return
|
||||
}
|
||||
mod = mod.Where(dao.ImTenant.Columns().Id, tenantId)
|
||||
}
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.ImTenant.Columns().Id, in.Id)
|
||||
}
|
||||
@@ -70,7 +102,7 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
||||
if in.Name != "" {
|
||||
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
|
||||
}
|
||||
if in.DbName != "" {
|
||||
if in.DbName != "" && isSuperTenantAdmin(ctx) {
|
||||
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
|
||||
}
|
||||
if in.State != nil {
|
||||
@@ -95,6 +127,12 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
||||
if err = s.fillListConfig(ctx, list); err != nil {
|
||||
return
|
||||
}
|
||||
// 业务库名仅超管可见,非超管脱敏返回
|
||||
if !isSuperTenantAdmin(ctx) {
|
||||
for _, item := range list {
|
||||
item.DbName = ""
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,13 +142,13 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
|
||||
ids = append(ids, item.Id)
|
||||
}
|
||||
|
||||
var configs []*entity.ImTenantConfig
|
||||
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]*entity.ImTenantConfig, len(configs))
|
||||
configMap := make(map[int64]*imTenantConfigModeration, len(configs))
|
||||
for _, item := range configs {
|
||||
configMap[item.TenantId] = item
|
||||
}
|
||||
@@ -127,24 +165,71 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
|
||||
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 = dsnMap[item.Id] != ""
|
||||
item.DbDsnConfigured = super && dsnMap[item.Id] != ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增IM租户
|
||||
// Add 新增IM租户
|
||||
func (s *sSysImTenant) Add(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("只有平台管理员可以新增商户")
|
||||
}
|
||||
in.Id = 0
|
||||
return s.saveTenant(ctx, in)
|
||||
}
|
||||
|
||||
// Edit 修改IM租户
|
||||
func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("商户ID不能为空")
|
||||
}
|
||||
if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) {
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
if tenantId <= 0 {
|
||||
return gerror.New("当前管理员未关联IM租户")
|
||||
}
|
||||
if in.Id != tenantId {
|
||||
return gerror.New("无权编辑其它商户")
|
||||
}
|
||||
}
|
||||
return s.saveTenant(ctx, in)
|
||||
}
|
||||
|
||||
return imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
// isSuperTenantAdmin 当前操作者是否为admin超管
|
||||
func isSuperTenantAdmin(ctx context.Context) bool {
|
||||
return service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx))
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
|
||||
super := isSuperTenantAdmin(ctx)
|
||||
isCreate := in.Id == 0
|
||||
tenantID := in.Id
|
||||
|
||||
// 业务库名与独立连接串仅超管可维护,非超管提交一律强制忽略
|
||||
if !super {
|
||||
in.DbName = ""
|
||||
in.DbDsn = ""
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -152,22 +237,168 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if in.Id > 0 {
|
||||
in.UpdatedBy = userId
|
||||
data := g.Map{
|
||||
dao.ImTenant.Columns().Code: in.Code,
|
||||
dao.ImTenant.Columns().Name: in.Name,
|
||||
dao.ImTenant.Columns().TenantDesc: in.TenantDesc,
|
||||
dao.ImTenant.Columns().State: in.State,
|
||||
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).
|
||||
Fields(sysin.ImTenantUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
||||
}
|
||||
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
|
||||
return s.saveConfig(ctx, in, userId)
|
||||
}
|
||||
|
||||
in.CreatedBy = userId
|
||||
in.UpdatedBy = userId
|
||||
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(sysin.ImTenantInsertFields{}).
|
||||
Data(in).InsertAndGetId()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
||||
}
|
||||
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
|
||||
tenantID = id
|
||||
in.Id = id
|
||||
return s.saveConfig(ctx, in, userId)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 新增商户时自动创建绑定的后台管理员账号,失败则补偿回滚商户数据
|
||||
if isCreate {
|
||||
if err = s.createTenantAdminMember(ctx, super, in, tenantID); err != nil {
|
||||
s.rollbackTenantCreation(ctx, tenantID)
|
||||
return err
|
||||
}
|
||||
} else if in.AdminPassword != "" {
|
||||
// 编辑商户时填写了新密码则重置管理员密码,留空不修改
|
||||
if err = s.resetTenantAdminPassword(ctx, tenantID, in.AdminPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = publishImPolicyEvent(ctx, tenantID, in.Code, "tenant.features.updated", "moderation", 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// createTenantAdminMember 为新增商户自动创建后台管理员账号(默认绑定imManage角色)
|
||||
func (s *sSysImTenant) createTenantAdminMember(ctx context.Context, super bool, in *sysin.ImTenantEditInp, tenantId int64) (err error) {
|
||||
var role *entity.AdminRole
|
||||
if err = dao.AdminRole.Ctx(ctx).
|
||||
Fields(dao.AdminRole.Columns().Id).
|
||||
Where(dao.AdminRole.Columns().Key, imManageRoleKey).
|
||||
Where(dao.AdminRole.Columns().Status, consts.StatusEnabled).
|
||||
OrderAsc(dao.AdminRole.Columns().Id).
|
||||
Scan(&role); err != nil {
|
||||
return gerror.Wrap(err, "查询IM管理员角色失败,请稍后重试!")
|
||||
}
|
||||
if role == nil || role.Id <= 0 {
|
||||
return gerror.New("IM管理员角色(imManage)未配置,请联系运维")
|
||||
}
|
||||
|
||||
// 部门挂载:超管挂公司部门;非超管受部门数据权限限制,只能从自己部门的下级中选取
|
||||
var deptId int64
|
||||
if super {
|
||||
companyDeptId, deptErr := dao.AdminDept.Ctx(ctx).
|
||||
Fields(dao.AdminDept.Columns().Id).
|
||||
Where(dao.AdminDept.Columns().Type, consts.DeptTypeCompany).
|
||||
Where(dao.AdminDept.Columns().Status, consts.StatusEnabled).
|
||||
OrderAsc(dao.AdminDept.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if deptErr != nil {
|
||||
return gerror.Wrap(deptErr, "获取公司部门失败,请稍后重试!")
|
||||
}
|
||||
if companyDeptId.IsEmpty() {
|
||||
return gerror.New("请先配置公司部门")
|
||||
}
|
||||
deptId = companyDeptId.Int64()
|
||||
} else {
|
||||
mb := contexts.GetUser(ctx)
|
||||
if mb == nil || mb.DeptId <= 0 {
|
||||
return gerror.New("获取当前用户部门失败")
|
||||
}
|
||||
subDeptId, deptErr := dao.AdminDept.Ctx(ctx).
|
||||
Fields(dao.AdminDept.Columns().Id).
|
||||
Where(dao.AdminDept.Columns().Status, consts.StatusEnabled).
|
||||
WhereNot(dao.AdminDept.Columns().Id, mb.DeptId).
|
||||
WhereLike(dao.AdminDept.Columns().Tree, "%"+tree.GetIdLabel(mb.DeptId)+"%").
|
||||
OrderAsc(dao.AdminDept.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if deptErr != nil {
|
||||
return gerror.Wrap(deptErr, "获取下级部门失败,请稍后重试!")
|
||||
}
|
||||
if subDeptId.IsEmpty() {
|
||||
return gerror.New("当前部门下没有可用的下级部门来挂载商户管理员账号,请先配置下级部门")
|
||||
}
|
||||
deptId = subDeptId.Int64()
|
||||
}
|
||||
|
||||
// 透传底层可读错误,便于前端定位
|
||||
return service.AdminMember().Edit(ctx, &adminin.MemberEditInp{
|
||||
TenantId: tenantId,
|
||||
RoleId: role.Id,
|
||||
Username: in.AdminUsername,
|
||||
Password: in.AdminPassword,
|
||||
RealName: in.Name + "管理员",
|
||||
DeptId: deptId,
|
||||
Status: consts.StatusEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
// resetTenantAdminPassword 重置商户管理员账号的密码(编辑弹窗填写新密码时调用)
|
||||
func (s *sSysImTenant) resetTenantAdminPassword(ctx context.Context, tenantId int64, password string) (err error) {
|
||||
roleVal, err := dao.AdminRole.Ctx(ctx).
|
||||
Fields(dao.AdminRole.Columns().Id).
|
||||
Where(dao.AdminRole.Columns().Key, imManageRoleKey).
|
||||
Where(dao.AdminRole.Columns().Status, consts.StatusEnabled).
|
||||
Limit(1).
|
||||
Value()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "查询IM管理员角色失败,请稍后重试!")
|
||||
}
|
||||
if roleVal.IsEmpty() {
|
||||
return gerror.New("IM管理员角色(imManage)未配置,请联系运维")
|
||||
}
|
||||
|
||||
memberId, err := dao.AdminMember.Ctx(ctx).
|
||||
Fields(dao.AdminMember.Columns().Id).
|
||||
Where(dao.AdminMember.Columns().TenantId, tenantId).
|
||||
Where(dao.AdminMember.Columns().RoleId, roleVal.Int64()).
|
||||
OrderAsc(dao.AdminMember.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "查询商户管理员账号失败,请稍后重试!")
|
||||
}
|
||||
if memberId.IsEmpty() {
|
||||
return gerror.New("该商户暂无管理员账号,无法重置密码")
|
||||
}
|
||||
|
||||
return service.AdminMember().ResetPwd(ctx, &adminin.MemberResetPwdInp{
|
||||
Id: memberId.Int64(),
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
|
||||
// rollbackTenantCreation 商户创建补偿:删除已写入的商户及配置记录
|
||||
func (s *sSysImTenant) rollbackTenantCreation(ctx context.Context, tenantId int64) {
|
||||
_ = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
WherePri(tenantId).Delete(); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).Delete()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,18 +417,34 @@ func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code strin
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, userId int64) (err error) {
|
||||
func (s *sSysImTenant) saveConfig(ctx context.Context, in *sysin.ImTenantEditInp, userId int64) (err error) {
|
||||
if in.MessageEncryptionEnabled == 1 {
|
||||
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
||||
Where("tenant_id", in.Id).
|
||||
Where("status", consts.ImMessageKeyStatusActive).
|
||||
Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥")
|
||||
}
|
||||
}
|
||||
var conf *entity.ImTenantConfig
|
||||
if err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||
Scan(&conf); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
|
||||
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
|
||||
@@ -206,6 +453,10 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
||||
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 {
|
||||
@@ -214,15 +465,21 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
||||
return
|
||||
}
|
||||
|
||||
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
|
||||
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租户配置失败,请稍后重试!")
|
||||
@@ -243,7 +500,7 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
||||
return
|
||||
}
|
||||
|
||||
var conf *entity.ImTenantConfig
|
||||
var conf *imTenantConfigModeration
|
||||
if err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||
Scan(&conf); err != nil {
|
||||
@@ -254,8 +511,37 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
||||
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).
|
||||
Fields(dao.AdminRole.Columns().Id).
|
||||
Where(dao.AdminRole.Columns().Key, imManageRoleKey).
|
||||
Where(dao.AdminRole.Columns().Status, consts.StatusEnabled).
|
||||
Limit(1).
|
||||
Value()
|
||||
if roleErr == nil && !roleVal.IsEmpty() {
|
||||
userVal, userErr := dao.AdminMember.Ctx(ctx).
|
||||
Fields(dao.AdminMember.Columns().Username).
|
||||
Where(dao.AdminMember.Columns().TenantId, in.Id).
|
||||
Where(dao.AdminMember.Columns().RoleId, roleVal.Int64()).
|
||||
OrderAsc(dao.AdminMember.Columns().Id).
|
||||
Limit(1).
|
||||
Value()
|
||||
if userErr == nil && !userVal.IsEmpty() {
|
||||
res.AdminUsername = userVal.String()
|
||||
}
|
||||
}
|
||||
// 业务库名与独立连接串仅超管可见,非超管全部脱敏
|
||||
if !isSuperTenantAdmin(ctx) {
|
||||
res.DbName = ""
|
||||
res.DbDsn = ""
|
||||
res.DbDsnConfigured = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -295,6 +581,11 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r
|
||||
DbDsnConfigured: tenant.DbDsn != "",
|
||||
Warnings: []string{},
|
||||
}
|
||||
// 业务库名与连接串信息仅超管可见,非超管全部脱敏
|
||||
if !isSuperTenantAdmin(ctx) {
|
||||
res.DbName = ""
|
||||
res.DbDsnConfigured = false
|
||||
}
|
||||
|
||||
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
|
||||
if dbErr != nil {
|
||||
|
||||
@@ -206,8 +206,13 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
||||
values.Set("user_ids", strings.Join(userIds, ","))
|
||||
}
|
||||
if in.UserId != "" {
|
||||
// 长数字按用户ID(u.id)精确匹配,短数字按后台展示编号(account_no)匹配
|
||||
if len(in.UserId) >= 12 {
|
||||
values.Set("user_id", in.UserId)
|
||||
} else {
|
||||
values.Set("account_no", in.UserId)
|
||||
}
|
||||
}
|
||||
if in.Nickname != "" {
|
||||
values.Set("nickname", in.Nickname)
|
||||
}
|
||||
@@ -360,6 +365,7 @@ func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
"up.public_account",
|
||||
"up.gender",
|
||||
"up.region",
|
||||
"COALESCE(up.friend_add_enabled, 1) friend_add_enabled",
|
||||
"us.auth_version",
|
||||
"us.password_updated_at",
|
||||
)
|
||||
@@ -589,6 +595,54 @@ func (s *sSysImUser) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGrou
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) FriendAdd(ctx context.Context, in *sysin.ImUserFriendAddInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := gtime.Now()
|
||||
result, err := db.Model("im_user_profiles").Safe().Ctx(ctx).
|
||||
Where("user_id", in.UserId).
|
||||
Data(g.Map{
|
||||
"friend_add_enabled": in.Enabled,
|
||||
"friend_add_disabled_reason": in.Reason,
|
||||
"friend_add_policy_version": gdb.Raw("friend_add_policy_version+1"),
|
||||
"updated_at": now,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "设置用户加好友开关失败")
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
profileID, idErr := imtenant.NextSnowflakeID(ctx)
|
||||
if idErr != nil {
|
||||
return gerror.Wrap(idErr, "生成用户资料ID失败")
|
||||
}
|
||||
_, err = db.Model("im_user_profiles").Safe().Ctx(ctx).Data(g.Map{
|
||||
"id": profileID,
|
||||
"user_id": in.UserId,
|
||||
"public_account": fmt.Sprintf("usr%d", in.UserId),
|
||||
"friend_add_enabled": in.Enabled,
|
||||
"friend_add_disabled_reason": in.Reason,
|
||||
"friend_add_policy_version": 1,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "补全用户资料并设置加好友开关失败")
|
||||
}
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "im_user_friend_add_update", "user", in.UserId, "success", g.Map{"enabled": in.Enabled, "reason": in.Reason})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) IPWhitelist(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (*sysin.ImUserIPWhitelistModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,18 +2,15 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/imworkbench"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"net/url"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const imWorkbenchAppsTable = "im_workbench_apps"
|
||||
|
||||
type sSysImWorkbench struct{}
|
||||
|
||||
func NewSysImWorkbench() *sSysImWorkbench { return &sSysImWorkbench{} }
|
||||
@@ -26,47 +23,46 @@ func (s *sSysImWorkbench) List(ctx context.Context, in *sysin.ImWorkbenchAppList
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
||||
return
|
||||
}
|
||||
mod := s.model(ctx, db)
|
||||
values := url.Values{}
|
||||
values.Set("page", intToString(in.Page))
|
||||
values.Set("per_page", intToString(in.PerPage))
|
||||
if in.Code != "" {
|
||||
mod = mod.WhereLike("code", "%"+in.Code+"%")
|
||||
values.Set("code", in.Code)
|
||||
}
|
||||
if in.Name != "" {
|
||||
mod = mod.WhereLike("name", "%"+in.Name+"%")
|
||||
values.Set("name", in.Name)
|
||||
}
|
||||
if in.Enabled != nil {
|
||||
mod = mod.Where("enabled", *in.Enabled)
|
||||
values.Set("enabled", intToString(*in.Enabled))
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("sort_order").OrderAsc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM工作台模块失败,请稍后重试!")
|
||||
return
|
||||
var page imworkbench.Page[sysin.ImWorkbenchAppModel]
|
||||
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/apps", values, &page); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, item := range list {
|
||||
list = make([]*sysin.ImWorkbenchAppModel, 0, len(page.Items))
|
||||
for i := range page.Items {
|
||||
item := page.Items[i]
|
||||
item.TenantId = tenant.Id
|
||||
item.TenantName = tenant.Name
|
||||
list = append(list, &item)
|
||||
}
|
||||
return
|
||||
return list, page.Total, nil
|
||||
}
|
||||
|
||||
func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
||||
return
|
||||
}
|
||||
data := g.Map{
|
||||
"id": in.Id,
|
||||
"code": in.Code,
|
||||
"name": in.Name,
|
||||
"icon_file_id": in.IconFileId,
|
||||
@@ -75,25 +71,18 @@ func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEdit
|
||||
"unread_key": in.UnreadKey,
|
||||
"enabled": in.Enabled,
|
||||
"permission": in.Permission,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
action := "im_workbench_app_create"
|
||||
targetId := in.Id
|
||||
if in.Id > 0 {
|
||||
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新IM工作台模块失败,请稍后重试!")
|
||||
}
|
||||
action = "im_workbench_app_update"
|
||||
} else {
|
||||
data["created_at"] = gtime.Now()
|
||||
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增IM工作台模块失败,请稍后重试!")
|
||||
}
|
||||
idValue, err := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields("id").Where("code", in.Code).Value()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取IM工作台模块ID失败,请稍后重试!")
|
||||
var res sysin.ImWorkbenchAppModel
|
||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps", data, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
targetId = idValue.Int64()
|
||||
if targetId <= 0 {
|
||||
targetId = res.Id
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_workbench_app", targetId, "success", g.Map{
|
||||
"code": in.Code, "route": in.Route, "enabled": in.Enabled, "permission": in.Permission,
|
||||
@@ -105,21 +94,12 @@ func (s *sSysImWorkbench) Delete(ctx context.Context, in *sysin.ImWorkbenchAppDe
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx)
|
||||
if in.Id > 0 {
|
||||
mod = mod.WherePri(in.Id)
|
||||
} else {
|
||||
mod = mod.Where("code", in.Code)
|
||||
}
|
||||
if _, err = mod.Data(g.Map{"enabled": consts.ImWorkbenchAppDisabled, "updated_at": gtime.Now()}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "禁用IM工作台模块失败,请稍后重试!")
|
||||
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
|
||||
return nil
|
||||
@@ -129,66 +109,23 @@ func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppR
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
||||
return
|
||||
}
|
||||
if err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
items := make([]g.Map, 0, len(in.Items))
|
||||
for _, item := range in.Items {
|
||||
if _, e := tx.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item.Code).
|
||||
Data(g.Map{"sort_order": item.SortOrder, "updated_at": gtime.Now()}).Update(); e != nil {
|
||||
return e
|
||||
items = append(items, g.Map{
|
||||
"code": item.Code,
|
||||
"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.Error() == "" {
|
||||
return gerror.New("调整IM工作台模块排序失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return gerror.Wrap(err, "调整IM工作台模块排序失败,请稍后重试!")
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_reorder", "im_workbench_app", 0, "success", g.Map{"count": len(in.Items)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImWorkbench) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields(
|
||||
"id",
|
||||
"code",
|
||||
"name",
|
||||
"icon_file_id",
|
||||
"route",
|
||||
"sort_order",
|
||||
"unread_key",
|
||||
"enabled",
|
||||
"permission",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImWorkbench) ensureDefaultApps(ctx context.Context, db gdb.DB) (err error) {
|
||||
defaults := []g.Map{
|
||||
{"code": "organization", "name": "组织架构", "route": consts.ImWorkbenchRouteOrganization, "sort_order": 10, "unread_key": "organization", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
|
||||
{"code": "meetings", "name": "会议通知", "route": consts.ImWorkbenchRouteMeetings, "sort_order": 20, "unread_key": "meetings", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
|
||||
{"code": "daily_reports", "name": "日报", "route": consts.ImWorkbenchRouteDailyReports, "sort_order": 30, "unread_key": "daily_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
|
||||
{"code": "monthly_reports", "name": "月报", "route": consts.ImWorkbenchRouteMonthlyReports, "sort_order": 40, "unread_key": "monthly_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
|
||||
{"code": "executive_mailbox", "name": "总经理信箱", "route": consts.ImWorkbenchRouteExecutiveMailbox, "sort_order": 50, "unread_key": "executive_mailbox", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
|
||||
}
|
||||
now := gtime.Now()
|
||||
for _, item := range defaults {
|
||||
var count int
|
||||
if count, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item["code"]).Count(); err != nil {
|
||||
return gerror.Wrap(err, "检查IM工作台默认模块失败,请稍后重试!")
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
item["created_at"] = now
|
||||
item["updated_at"] = now
|
||||
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "初始化IM工作台默认模块失败,请稍后重试!")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// =================================================================================
|
||||
// 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 //
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// =================================================================================
|
||||
// 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:""`
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"hotgo/internal/model/input/form"
|
||||
)
|
||||
|
||||
type CultivateContentListInp struct {
|
||||
form.PageReq
|
||||
ObjectType string `json:"objectType" d:"feed_post"`
|
||||
AuthorId string `json:"authorId"`
|
||||
Status string `json:"status"`
|
||||
Keyword string `json:"keyword"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (in *CultivateContentListInp) Filter(context.Context) error {
|
||||
in.ObjectType = strings.TrimSpace(in.ObjectType)
|
||||
if in.ObjectType != "feed_post" && in.ObjectType != "feed_comment" {
|
||||
return gerror.New("内容类型不正确")
|
||||
}
|
||||
if in.AuthorId != "" {
|
||||
if _, err := strconv.ParseUint(in.AuthorId, 10, 64); err != nil {
|
||||
return gerror.New("作者ID不正确")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateContentListModel struct {
|
||||
Id string `json:"id"`
|
||||
ObjectType string `json:"objectType"`
|
||||
PostId string `json:"postId"`
|
||||
AuthorId string `json:"authorId"`
|
||||
ContentText string `json:"contentText"`
|
||||
FlowerCode string `json:"flowerCode"`
|
||||
Visibility string `json:"visibility"`
|
||||
Status string `json:"status"`
|
||||
AuditStatus string `json:"auditStatus"`
|
||||
RejectReason string `json:"rejectReason"`
|
||||
LikeCount uint `json:"likeCount"`
|
||||
CommentCount uint `json:"commentCount"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type CultivateContentViewInp struct {
|
||||
ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"`
|
||||
Id string `json:"id" v:"required#内容ID不能为空"`
|
||||
ObjectId uint64 `json:"-"`
|
||||
}
|
||||
|
||||
func (in *CultivateContentViewInp) Filter(context.Context) error {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return gerror.New("内容ID不正确")
|
||||
}
|
||||
in.ObjectId = id
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateMediaModel struct {
|
||||
Id string `json:"id"`
|
||||
FileId string `json:"fileId"`
|
||||
ThumbnailFileId string `json:"thumbnailFileId"`
|
||||
MediaType string `json:"mediaType"`
|
||||
AuditStatus string `json:"auditStatus"`
|
||||
SortNo uint `json:"sortNo"`
|
||||
}
|
||||
type CultivateAuditModel struct {
|
||||
Id string `json:"id"`
|
||||
AuditSource string `json:"auditSource"`
|
||||
StatusBefore string `json:"statusBefore"`
|
||||
StatusAfter string `json:"statusAfter"`
|
||||
ResultCode string `json:"resultCode"`
|
||||
RiskLabels any `json:"riskLabels"`
|
||||
OperatorId string `json:"operatorId"`
|
||||
OccurredAt *gtime.Time `json:"occurredAt"`
|
||||
}
|
||||
type CultivateContentViewModel struct {
|
||||
CultivateContentListModel
|
||||
AuthorNickname string `json:"authorNickname"`
|
||||
AuthorAvatarFileId string `json:"authorAvatarFileId"`
|
||||
Media []*CultivateMediaModel `json:"media"`
|
||||
Audits []*CultivateAuditModel `json:"audits"`
|
||||
RelatedReports []*CultivateReportListModel `json:"relatedReports"`
|
||||
}
|
||||
|
||||
type CultivateContentReviewInp struct {
|
||||
ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"`
|
||||
Id string `json:"id" v:"required#内容ID不能为空"`
|
||||
Action string `json:"action" v:"required|in:approve,reject,take_down,restore#审核动作不能为空|审核动作不正确"`
|
||||
Reason string `json:"reason"`
|
||||
ObjectId uint64 `json:"-"`
|
||||
}
|
||||
|
||||
type CultivateContentBatchReviewInp struct {
|
||||
ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"`
|
||||
Ids []string `json:"ids" v:"required#请选择内容"`
|
||||
Action string `json:"action" v:"required|in:approve,reject,take_down,restore#审核动作不能为空|审核动作不正确"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (in *CultivateContentBatchReviewInp) Filter(ctx context.Context) error {
|
||||
if len(in.Ids) == 0 || len(in.Ids) > 100 {
|
||||
return gerror.New("单次批量处理数量必须为1至100条")
|
||||
}
|
||||
in.Reason = strings.TrimSpace(in.Reason)
|
||||
if (in.Action == "reject" || in.Action == "take_down") && in.Reason == "" {
|
||||
return gerror.New("拒绝或下架时必须填写原因")
|
||||
}
|
||||
for _, id := range in.Ids {
|
||||
item := &CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason}
|
||||
if err := item.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateContentBatchReviewModel struct {
|
||||
SuccessCount int `json:"successCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
Failures map[string]string `json:"failures"`
|
||||
}
|
||||
|
||||
func (in *CultivateContentReviewInp) Filter(context.Context) error {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return gerror.New("内容ID不正确")
|
||||
}
|
||||
in.ObjectId = id
|
||||
in.Reason = strings.TrimSpace(in.Reason)
|
||||
if (in.Action == "reject" || in.Action == "take_down") && in.Reason == "" {
|
||||
return gerror.New("拒绝或下架时必须填写原因")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateReportListInp struct {
|
||||
form.PageReq
|
||||
ObjectType string `json:"objectType"`
|
||||
Status string `json:"status"`
|
||||
ReporterId string `json:"reporterId"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (in *CultivateReportListInp) Filter(context.Context) error {
|
||||
if in.ReporterId != "" {
|
||||
if _, err := strconv.ParseUint(in.ReporterId, 10, 64); err != nil {
|
||||
return gerror.New("举报人ID不正确")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateReportListModel struct {
|
||||
Id string `json:"id"`
|
||||
ReporterId string `json:"reporterId"`
|
||||
ReportedUserId string `json:"reportedUserId"`
|
||||
ObjectType string `json:"objectType"`
|
||||
ObjectId string `json:"objectId"`
|
||||
ReasonCode string `json:"reasonCode"`
|
||||
Description string `json:"description"`
|
||||
EvidenceFileIds any `json:"evidenceFileIds"`
|
||||
SourcePage string `json:"sourcePage"`
|
||||
Status string `json:"status"`
|
||||
HandlerId string `json:"handlerId"`
|
||||
HandleResult string `json:"handleResult"`
|
||||
HandledAt *gtime.Time `json:"handledAt"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CultivateReportViewInp struct {
|
||||
Id string `json:"id" v:"required#举报ID不能为空"`
|
||||
ReportId uint64 `json:"-"`
|
||||
}
|
||||
|
||||
func (in *CultivateReportViewInp) Filter(context.Context) error {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return gerror.New("举报ID不正确")
|
||||
}
|
||||
in.ReportId = id
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateReportViewModel struct {
|
||||
CultivateReportListModel
|
||||
ReporterNickname string `json:"reporterNickname"`
|
||||
ReportedUserNickname string `json:"reportedUserNickname"`
|
||||
TargetContent *CultivateContentListModel `json:"targetContent"`
|
||||
History []*CultivateReportListModel `json:"history"`
|
||||
}
|
||||
|
||||
type CultivateReportHandleInp struct {
|
||||
Id string `json:"id" v:"required#举报ID不能为空"`
|
||||
Action string `json:"action" v:"required|in:processing,resolve,reject#处理动作不能为空|处理动作不正确"`
|
||||
HandleResult string `json:"handleResult"`
|
||||
ContentAction string `json:"contentAction" v:"in:none,take_down#内容处置不正确"`
|
||||
UserAction string `json:"userAction" v:"in:none,freeze,ban#用户处置不正确"`
|
||||
Reason string `json:"reason"`
|
||||
ReportId uint64 `json:"-"`
|
||||
}
|
||||
|
||||
func (in *CultivateReportHandleInp) Filter(context.Context) error {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return gerror.New("举报ID不正确")
|
||||
}
|
||||
in.ReportId = id
|
||||
in.HandleResult = strings.TrimSpace(in.HandleResult)
|
||||
in.ContentAction = strings.TrimSpace(in.ContentAction)
|
||||
in.UserAction = strings.TrimSpace(in.UserAction)
|
||||
in.Reason = strings.TrimSpace(in.Reason)
|
||||
if in.ContentAction == "" {
|
||||
in.ContentAction = "none"
|
||||
}
|
||||
if in.UserAction == "" {
|
||||
in.UserAction = "none"
|
||||
}
|
||||
if in.Action != "processing" && in.HandleResult == "" {
|
||||
return gerror.New("办结举报时必须填写处理结果")
|
||||
}
|
||||
if (in.ContentAction != "none" || in.UserAction != "none") && in.Reason == "" {
|
||||
return gerror.New("联动处置时必须填写处置原因")
|
||||
}
|
||||
if in.Action != "resolve" && (in.ContentAction != "none" || in.UserAction != "none") {
|
||||
return gerror.New("仅确认处理时可执行内容或用户处置")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateOperationLogListInp struct {
|
||||
form.PageReq
|
||||
OperatorId string `json:"operatorId"`
|
||||
OperationType string `json:"operationType"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetId string `json:"targetId"`
|
||||
RequestId string `json:"requestId"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CultivateDashboardInp struct{}
|
||||
|
||||
func (*CultivateDashboardInp) Filter(context.Context) error { return nil }
|
||||
|
||||
type CultivateDashboardModel struct {
|
||||
PlantCount int64 `json:"plantCount"`
|
||||
GrowingCount int64 `json:"growingCount"`
|
||||
BloomCount int64 `json:"bloomCount"`
|
||||
HarvestedCount int64 `json:"harvestedCount"`
|
||||
RemovedCount int64 `json:"removedCount"`
|
||||
Active7d int64 `json:"active7d"`
|
||||
Active30d int64 `json:"active30d"`
|
||||
PostCount int64 `json:"postCount"`
|
||||
PublicPostCount int64 `json:"publicPostCount"`
|
||||
FriendsPostCount int64 `json:"friendsPostCount"`
|
||||
PrivatePostCount int64 `json:"privatePostCount"`
|
||||
CommentCount int64 `json:"commentCount"`
|
||||
LikeCount int64 `json:"likeCount"`
|
||||
TakenDownCount int64 `json:"takenDownCount"`
|
||||
PendingAuditCount int64 `json:"pendingAuditCount"`
|
||||
ApprovedAuditCount int64 `json:"approvedAuditCount"`
|
||||
RejectedAuditCount int64 `json:"rejectedAuditCount"`
|
||||
PendingReportCount int64 `json:"pendingReportCount"`
|
||||
ProcessingReportCount int64 `json:"processingReportCount"`
|
||||
ProcessedReportCount int64 `json:"processedReportCount"`
|
||||
}
|
||||
|
||||
func (*CultivateOperationLogListInp) Filter(context.Context) error { return nil }
|
||||
|
||||
type CultivateOperationLogListModel struct {
|
||||
Id string `json:"id"`
|
||||
OperatorId string `json:"operatorId"`
|
||||
OperationType string `json:"operationType"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetId string `json:"targetId"`
|
||||
RequestId string `json:"requestId"`
|
||||
BeforeSnapshot any `json:"beforeSnapshot"`
|
||||
AfterSnapshot any `json:"afterSnapshot"`
|
||||
Reason string `json:"reason"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCultivateContentBatchReviewFilter(t *testing.T) {
|
||||
in := &CultivateContentBatchReviewInp{ObjectType: "feed_post", Ids: []string{"123"}, Action: "take_down", Reason: "违规内容"}
|
||||
if err := in.Filter(context.Background()); err != nil {
|
||||
t.Fatalf("Filter returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCultivateContentBatchReviewFilterRequiresReason(t *testing.T) {
|
||||
in := &CultivateContentBatchReviewInp{ObjectType: "feed_post", Ids: []string{"123"}, Action: "reject"}
|
||||
if err := in.Filter(context.Background()); err == nil {
|
||||
t.Fatal("expected missing reason error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCultivateReportHandleFilterGovernanceRules(t *testing.T) {
|
||||
in := &CultivateReportHandleInp{Id: "123", Action: "processing", UserAction: "freeze", Reason: "恶意行为"}
|
||||
if err := in.Filter(context.Background()); err == nil {
|
||||
t.Fatal("expected governance action to require resolve")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"hotgo/internal/model/input/form"
|
||||
)
|
||||
|
||||
type CultivateStageRuleEditInp struct {
|
||||
Id uint64 `json:"id"`
|
||||
Stage string `json:"stage"`
|
||||
NextStage string `json:"nextStage"`
|
||||
RequiredPoints uint `json:"requiredPoints"`
|
||||
WaterPoints uint `json:"waterPoints"`
|
||||
FertilizePoints uint `json:"fertilizePoints"`
|
||||
AllowedActions []string `json:"allowedActions"`
|
||||
ImageFileId string `json:"imageFileId"`
|
||||
AnimationFileId string `json:"animationFileId"`
|
||||
ProgressTextTemplate string `json:"progressTextTemplate"`
|
||||
CompletionText string `json:"completionText"`
|
||||
SortNo int `json:"sortNo"`
|
||||
}
|
||||
|
||||
type CultivateFlowerEditInp struct {
|
||||
Id uint64 `json:"id"`
|
||||
FlowerCode string `json:"flowerCode"`
|
||||
FlowerName string `json:"flowerName"`
|
||||
FlowerDesc string `json:"flowerDesc"`
|
||||
IconFileId string `json:"iconFileId"`
|
||||
Status string `json:"status"`
|
||||
DailyWaterLimit uint `json:"dailyWaterLimit"`
|
||||
DailyFertilizeLimit uint `json:"dailyFertilizeLimit"`
|
||||
ShareConfig *gjson.Json `json:"shareConfig"`
|
||||
SortNo int `json:"sortNo"`
|
||||
StageRules []*CultivateStageRuleEditInp `json:"stageRules"`
|
||||
}
|
||||
|
||||
func (in *CultivateFlowerEditInp) Filter(ctx context.Context) error {
|
||||
in.FlowerCode = strings.TrimSpace(in.FlowerCode)
|
||||
in.FlowerName = strings.TrimSpace(in.FlowerName)
|
||||
if in.FlowerCode == "" || in.FlowerName == "" {
|
||||
return gerror.New("花种编码和名称不能为空")
|
||||
}
|
||||
validStatus := map[string]bool{"online": true, "offline": true, "activity_only": true, "maintenance": true}
|
||||
if !validStatus[in.Status] {
|
||||
return gerror.New("花种状态不正确")
|
||||
}
|
||||
if len(in.StageRules) == 0 {
|
||||
return gerror.New("至少配置一个成长阶段")
|
||||
}
|
||||
validStage := map[string]bool{"seed": true, "sprout": true, "bud": true, "bloom": true}
|
||||
validAction := map[string]bool{"water": true, "fertilize": true}
|
||||
seen := map[string]bool{}
|
||||
for _, rule := range in.StageRules {
|
||||
if rule == nil || !validStage[rule.Stage] {
|
||||
return gerror.New("成长阶段不正确")
|
||||
}
|
||||
if seen[rule.Stage] {
|
||||
return gerror.New("成长阶段不能重复")
|
||||
}
|
||||
seen[rule.Stage] = true
|
||||
if rule.Stage != "bloom" && rule.RequiredPoints == 0 {
|
||||
return gerror.New("非开花阶段升级阈值必须大于0")
|
||||
}
|
||||
if rule.NextStage != "" && !validStage[rule.NextStage] {
|
||||
return gerror.New("下一阶段不正确")
|
||||
}
|
||||
if rule.Stage != "bloom" && len(rule.AllowedActions) == 0 {
|
||||
return gerror.New("非开花阶段至少允许一种成长动作")
|
||||
}
|
||||
for _, action := range rule.AllowedActions {
|
||||
if !validAction[action] {
|
||||
return gerror.New("成长动作不正确")
|
||||
}
|
||||
}
|
||||
}
|
||||
stage := "seed"
|
||||
visited := map[string]bool{}
|
||||
for stage != "" {
|
||||
if visited[stage] {
|
||||
return gerror.New("成长阶段链不能形成循环")
|
||||
}
|
||||
visited[stage] = true
|
||||
var next string
|
||||
found := false
|
||||
for _, r := range in.StageRules {
|
||||
if r.Stage == stage {
|
||||
next = r.NextStage
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return gerror.New("成长阶段链必须从 seed 连续配置")
|
||||
}
|
||||
stage = next
|
||||
}
|
||||
if !visited["bloom"] {
|
||||
return gerror.New("成长阶段链必须以 bloom 结束")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CultivateFlowerListInp struct {
|
||||
form.PageReq
|
||||
FlowerCode string `json:"flowerCode"`
|
||||
FlowerName string `json:"flowerName"`
|
||||
Status string `json:"status"`
|
||||
ConfigVersion string `json:"configVersion"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (*CultivateFlowerListInp) Filter(context.Context) error { return nil }
|
||||
|
||||
type CultivateFlowerListModel struct {
|
||||
Id uint64 `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"`
|
||||
SortNo int `json:"sortNo"`
|
||||
StageCount int `json:"stageCount"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type CultivateFlowerViewInp struct {
|
||||
Id uint64 `json:"id" v:"required#ID不能为空"`
|
||||
}
|
||||
|
||||
func (*CultivateFlowerViewInp) Filter(context.Context) error { return nil }
|
||||
|
||||
type CultivateFlowerViewModel struct {
|
||||
CultivateFlowerEditInp
|
||||
ConfigVersion string `json:"configVersion"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type CultivateFlowerStatusInp struct {
|
||||
Id uint64 `json:"id" v:"required#ID不能为空"`
|
||||
Status string `json:"status" v:"required|in:online,offline,activity_only,maintenance#状态不能为空|状态不正确"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (*CultivateFlowerStatusInp) Filter(context.Context) error { return nil }
|
||||
@@ -76,6 +76,7 @@ type ImInviteBatchCreateInp struct {
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
||||
Remark string `json:"remark" dc:"备注"`
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
UserId int64 `json:"userId" dc:"绑定C端用户ID,非必填,>0时为批次内所有邀请码绑定该用户"`
|
||||
}
|
||||
|
||||
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
||||
@@ -92,6 +93,9 @@ func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
||||
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
|
||||
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
||||
}
|
||||
if in.UserId < 0 {
|
||||
return gerror.New("绑定用户ID不合法")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user