Compare commits
50
Commits
bd19a594ae
...
0596599db9
| 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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
image_tag:
|
image_tag:
|
||||||
description: '镜像tag,留空则使用commit短哈希'
|
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)'
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,26 +17,21 @@ jobs:
|
|||||||
id: vars
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ github.event.inputs.image_tag }}"
|
TAG="${{ github.event.inputs.image_tag }}"
|
||||||
[ -z "$TAG" ] && TAG=$(git rev-parse --short HEAD)
|
[ -z "$TAG" ] && TAG=test
|
||||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: 构建镜像(前端+后端一体,自包含多阶段构建)
|
- name: 登录镜像仓库
|
||||||
run: docker build -t im-manager:${{ steps.vars.outputs.tag }} .
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin
|
||||||
|
|
||||||
- name: 蓝绿部署
|
- 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 }}
|
|
||||||
run: |
|
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
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.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
|
RUN rm -rf resource/public/admin && mkdir -p resource/public/admin
|
||||||
COPY --from=web-build /web/dist/ ./resource/public/admin/
|
COPY --from=web-build /web/dist/ ./resource/public/admin/
|
||||||
|
|
||||||
# main.go does `import _ "hotgo/internal/packed"`, a file normally generated by
|
# main.go imports internal/packed. Generate the packed resource file directly
|
||||||
# `gf build` (see server/hack/config.yaml: packSrc/packDst) and gitignored —
|
# through GoFrame's library API instead of installing the full gf CLI in CI.
|
||||||
# generate it explicitly with the same gf CLI version pinned in go.mod.
|
RUN printf '%s\n' \
|
||||||
RUN go install github.com/gogf/gf/cmd/gf/v2@v2.10.0
|
'package main' \
|
||||||
RUN gf pack resource internal/packed/packed.go
|
'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
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
ENV QUEUE_DRIVER=kafka
|
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_VERSION=2.4.0
|
||||||
ENV KAFKA_USERNAME=kafka-external
|
ENV KAFKA_USERNAME=
|
||||||
ENV KAFKA_PASSWORD=gr58tNnlZOVGW3AmMbL2
|
ENV KAFKA_PASSWORD=
|
||||||
ENV KAFKA_SASL_MECHANISM=SCRAM-SHA-512
|
ENV KAFKA_SASL_MECHANISM=
|
||||||
ENV IM_ADMIN_API_BASE_URL=http://im-server:6060
|
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_SERVICE_ID=hotgo-admin
|
||||||
ENV IM_ADMIN_API_SECRET=8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b
|
ENV IM_ADMIN_API_SECRET=
|
||||||
ENV IM_INTERNAL_API_BASE_URL=http://im-server:6060
|
ENV IM_INTERNAL_API_BASE_URL=https://api.118im.com
|
||||||
ENV IM_INTERNAL_API_SERVICE_NAME=hotgo-admin
|
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_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/manifest ./manifest
|
||||||
COPY --from=server-build /src/hack ./hack
|
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`)
|
KEY `idx_im_login_failure_request` (`request_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) 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` (
|
CREATE TABLE IF NOT EXISTS `im_mailbox_config` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
`enabled` tinyint NOT NULL DEFAULT '1',
|
`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
|
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 {
|
type TenantReq struct {
|
||||||
g.Meta `path:"/internal/im-config/tenants/by-code/{code}" method:"get" tags:"IM内部配置" summary:"按编码获取IM租户"`
|
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:"配置值"`
|
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 {
|
type TenantLoginIPWhitelistReq struct {
|
||||||
g.Meta `path:"/internal/im-config/tenants/by-id/{id}/login-ip-whitelist" method:"get" tags:"IM内部配置" summary:"获取租户渠道登录IP白名单"`
|
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"`
|
Id int64 `json:"id" in:"path" dc:"租户ID"`
|
||||||
|
|||||||
@@ -263,83 +263,3 @@ type AdminLogListRes struct {
|
|||||||
form.PageRes
|
form.PageRes
|
||||||
List []*sysin.ImGroupAdminLogListModel `json:"list" dc:"数据列表"`
|
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
|
sysin.ImLoginFailureBanUserInp
|
||||||
}
|
}
|
||||||
type BanUserRes struct{ *sysin.ImUserCommandModel }
|
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 {
|
type RestoreDefaultRes struct {
|
||||||
Id int64 `json:"id"`
|
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 {
|
type IPRuleListReq struct {
|
||||||
g.Meta `path:"/imSecurityPolicy/ipRuleList" method:"get" tags:"IM安全策略" summary:"获取IP黑白名单"`
|
g.Meta `path:"/imSecurityPolicy/ipRuleList" method:"get" tags:"IM安全策略" summary:"获取IP黑白名单"`
|
||||||
sysin.ImSecurityIPRuleListInp
|
sysin.ImSecurityIPRuleListInp
|
||||||
|
|||||||
@@ -33,9 +33,17 @@ type ViewRes struct {
|
|||||||
*sysin.ImTenantViewModel
|
*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 {
|
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
|
sysin.ImTenantEditInp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ type AdjustGroupReq struct {
|
|||||||
|
|
||||||
type AdjustGroupRes 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 {
|
type IPWhitelistReq struct {
|
||||||
g.Meta `path:"/imUser/ipWhitelist" method:"get" tags:"IM用户" summary:"获取IM用户IP白名单"`
|
g.Meta `path:"/imUser/ipWhitelist" method:"get" tags:"IM用户" summary:"获取IM用户IP白名单"`
|
||||||
sysin.ImUserIPWhitelistInp
|
sysin.ImUserIPWhitelistInp
|
||||||
|
|||||||
+4
-3
@@ -4,10 +4,11 @@ go 1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/IBM/sarama v1.45.2
|
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/dysmsapi-20170525/v3 v3.0.6
|
||||||
github.com/alibabacloud-go/tea v1.3.9
|
github.com/alibabacloud-go/green-20220302/v3 v3.5.4
|
||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
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/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||||
github.com/apache/rocketmq-client-go/v2 v2.1.2
|
github.com/apache/rocketmq-client-go/v2 v2.1.2
|
||||||
github.com/casbin/casbin/v2 v2.108.0
|
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 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
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.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.2.4 h1:o6veen0IZ/Fe1JawwhwQMZcbw67CVDY1pQwXcNWPyQo=
|
||||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7/go.mod h1:TBpgqm3XofZz2LCYjZhektGPU7ArEgascyzbm4SjFo4=
|
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 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
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=
|
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/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 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
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.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 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
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.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
github.com/alibabacloud-go/tea v1.1.20/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.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.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
|
||||||
github.com/alibabacloud-go/tea v1.3.9 h1:bjgt1bvdY780vz/17iWNNtbXl4A77HWntWMeaUF3So0=
|
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
|
||||||
github.com/alibabacloud-go/tea v1.3.9/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
|
||||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
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 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.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.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.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.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
|
||||||
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/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||||
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
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 h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ gfcli:
|
|||||||
gJsonSupport: true
|
gJsonSupport: true
|
||||||
clear: false
|
clear: false
|
||||||
|
|
||||||
|
- link: "mysql:root:root@tcp(127.0.0.1:3306)/cultivate?loc=Local&parseTime=true&charset=utf8mb4"
|
||||||
|
group: "cultivate"
|
||||||
|
tables: "cv_flower_catalog,cv_flower_stage_rule,cv_feed_post,cv_feed_media,cv_feed_comment,cv_feed_report,cv_content_audit_log,cv_admin_operation_log"
|
||||||
|
removePrefix: "cv_"
|
||||||
|
descriptionTag: true
|
||||||
|
noModelComment: true
|
||||||
|
jsonCase: "CamelLower"
|
||||||
|
gJsonSupport: true
|
||||||
|
clear: false
|
||||||
|
|
||||||
|
|
||||||
# service: # 生成业务配置
|
# service: # 生成业务配置
|
||||||
# srcFolder: "internal/logic"
|
# srcFolder: "internal/logic"
|
||||||
|
|||||||
@@ -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 = " / " // 地区分隔符
|
RegionSpilt = " / " // 地区分隔符
|
||||||
Unknown = "Unknown" // Unknown
|
Unknown = "Unknown" // Unknown
|
||||||
SuperRoleKey = "super" // 超管角色唯一标识符,通过角色验证超管
|
SuperRoleKey = "super" // 超管角色唯一标识符,通过角色验证超管
|
||||||
|
ImPlatformRoleKey = "supreme_manager" // IM平台超管角色唯一标识符
|
||||||
MaxServeLogContentLen = 2048 // 最大保留服务日志内容大小
|
MaxServeLogContentLen = 2048 // 最大保留服务日志内容大小
|
||||||
SysDefaultLanguage = "zh_CN" // 系统默认语言,当配置文件没有国际化配置时生效
|
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)
|
res.PageRes.Pack(req, totalCount)
|
||||||
return
|
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)
|
data, err := service.SysImLoginFailureLog().BanUser(ctx, &req.ImLoginFailureBanUserInp)
|
||||||
return &imloginfailurelog.BanUserRes{ImUserCommandModel: data}, err
|
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
|
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) {
|
func (c *cImSecurityPolicy) IPRuleList(ctx context.Context, req *imsecuritypolicy.IPRuleListReq) (res *imsecuritypolicy.IPRuleListRes, err error) {
|
||||||
list, total, err := service.SysImSecurityPolicy().IPRuleList(ctx, &req.ImSecurityIPRuleListInp)
|
list, total, err := service.SysImSecurityPolicy().IPRuleList(ctx, &req.ImSecurityIPRuleListInp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ func (c *cImTenant) List(ctx context.Context, req *imtenant.ListReq) (res *imten
|
|||||||
return
|
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租户
|
// Edit 更新IM租户
|
||||||
func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) {
|
func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) {
|
||||||
err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp)
|
err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp)
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ func (c *cImUser) AdjustGroup(ctx context.Context, req *imuser.AdjustGroupReq) (
|
|||||||
return
|
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) {
|
func (c *cImUser) IPWhitelist(ctx context.Context, req *imuser.IPWhitelistReq) (res *imuser.IPWhitelistRes, err error) {
|
||||||
data, err := service.SysImUser().IPWhitelist(ctx, &req.ImUserIPWhitelistInp)
|
data, err := service.SysImUser().IPWhitelist(ctx, &req.ImUserIPWhitelistInp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func Serve(r *ghttp.Request) {
|
|||||||
checks := map[string]string{}
|
checks := map[string]string{}
|
||||||
statusCode := http.StatusOK
|
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()
|
checks["mysql"] = err.Error()
|
||||||
statusCode = http.StatusServiceUnavailable
|
statusCode = http.StatusServiceUnavailable
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package imconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
api "hotgo/api/admin/imconfiginternal"
|
api "hotgo/api/admin/imconfiginternal"
|
||||||
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
|
"hotgo/internal/library/immoderation"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,6 +18,18 @@ import (
|
|||||||
"github.com/gogf/gf/v2/util/gconv"
|
"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{}
|
var Config = cConfig{}
|
||||||
|
|
||||||
type cConfig struct{}
|
type cConfig struct{}
|
||||||
@@ -91,6 +107,375 @@ func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *
|
|||||||
return &api.ThirdPartyRes{Values: values}, nil
|
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) {
|
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 {
|
if _, err = getTenant(ctx, dao.ImTenant.Columns().Id, req.Id); err != nil {
|
||||||
return nil, err
|
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))
|
signature := strings.TrimSpace(r.Header.Get(headerSignature))
|
||||||
expectedService := g.Cfg().MustGet(ctx, "imConfigApi.serviceName", "im-server").String()
|
expectedService := g.Cfg().MustGet(ctx, "imConfigApi.serviceName", "im-server").String()
|
||||||
secret := configuredSecret(r)
|
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")
|
writeAuthError(r, http.StatusUnauthorized, "internal signature required")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -85,6 +85,19 @@ func Authorize(r *ghttp.Request) bool {
|
|||||||
return true
|
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 {
|
func configuredSecret(r *ghttp.Request) string {
|
||||||
ctx := r.GetCtx()
|
ctx := r.GetCtx()
|
||||||
secret := g.Cfg().MustGet(ctx, "imConfigApi.serviceSecret").String()
|
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)
|
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
||||||
}
|
}
|
||||||
if tenant.DbName == "" {
|
if tenant.DbName == "" {
|
||||||
return "", gerror.New("租户业务库未配置")
|
// 未配置独立业务库的商户回落共享默认库
|
||||||
|
defaultLink := defaultDbLink(ctx)
|
||||||
|
if defaultLink == "" {
|
||||||
|
return "", gerror.New("默认数据库连接未配置,无法回落共享业务库")
|
||||||
|
}
|
||||||
|
return normalizeMySQLTimeOptions(defaultLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultLink := defaultDbLink(ctx)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
|
if service.AdminMember().VerifySuperId(ctx, opMemberId) || contexts.GetRoleKey(ctx) == consts.ImPlatformRoleKey {
|
||||||
if in.TenantId > 0 {
|
if in.TenantId > 0 {
|
||||||
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||||
@@ -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 {
|
if err = service.AdminRole().VerifyRoleId(ctx, in.RoleId); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = s.normalizeTenantByRole(ctx, in); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 验证部门ID
|
// 验证部门ID
|
||||||
if err = service.AdminDept().VerifyDeptId(ctx, in.DeptId); err != nil {
|
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 获取用户信息
|
// View 获取用户信息
|
||||||
func (s *sAdminMember) View(ctx context.Context, in *adminin.MemberViewInp) (res *adminin.MemberViewModel, err error) {
|
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 {
|
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 {
|
func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb.Model {
|
||||||
m := dao.AdminMember.Ctx(ctx)
|
m := dao.AdminMember.Ctx(ctx)
|
||||||
if s.VerifySuperId(ctx, memberId) {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
var roleId int64
|
var roleId int64
|
||||||
if contexts.GetUserId(ctx) == memberId {
|
if contexts.GetUserId(ctx) == memberId {
|
||||||
@@ -929,6 +973,10 @@ func (s *sAdminMember) FilterAuthModel(ctx context.Context, memberId int64) *gdb
|
|||||||
roleId = ro.Int64()
|
roleId = ro.Int64()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAdminScopeSuper(ctx, memberId, roleId) {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
roleIds, err := service.AdminRole().GetSubRoleIds(ctx, roleId, false)
|
roleIds, err := service.AdminRole().GetSubRoleIds(ctx, roleId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Panicf(ctx, "get the subordinate role permission exception, err:%+v", err)
|
g.Log().Panicf(ctx, "get the subordinate role permission exception, err:%+v", err)
|
||||||
|
|||||||
@@ -37,6 +37,35 @@ func init() {
|
|||||||
service.RegisterAdminRole(NewAdminRole())
|
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 验证权限
|
// Verify 验证权限
|
||||||
func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
||||||
var (
|
var (
|
||||||
@@ -64,13 +93,19 @@ func (s *sAdminRole) Verify(ctx context.Context, path, method string) bool {
|
|||||||
// List 获取列表
|
// List 获取列表
|
||||||
func (s *sAdminRole) List(ctx context.Context, in *adminin.RoleListInp) (res *adminin.RoleListModel, totalCount int, err error) {
|
func (s *sAdminRole) List(ctx context.Context, in *adminin.RoleListInp) (res *adminin.RoleListModel, totalCount int, err error) {
|
||||||
var (
|
var (
|
||||||
mod = dao.AdminRole.Ctx(ctx)
|
mod = dao.AdminRole.Ctx(ctx).WhereNot(dao.AdminRole.Columns().Key, consts.SuperRoleKey)
|
||||||
models []*entity.AdminRole
|
models []*entity.AdminRole
|
||||||
pid int64 = 0
|
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)
|
pid = contexts.GetRoleId(ctx)
|
||||||
mod = mod.WhereLike(dao.AdminRole.Columns().Tree, "%"+tree.GetIdLabel(pid)+"%")
|
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
|
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 {
|
if err != nil {
|
||||||
err = gerror.New("验证角色信息失败!")
|
err = gerror.New("验证角色信息失败!")
|
||||||
return
|
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/library/dict"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
@@ -30,11 +31,18 @@ func init() {
|
|||||||
|
|
||||||
// Delete 删除
|
// Delete 删除
|
||||||
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
|
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()
|
_, err := dao.SysDictData.Ctx(ctx).Where("id", in.Id).Delete()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, oldTypes)
|
||||||
return nil
|
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) {
|
func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err error) {
|
||||||
// 修改
|
// 修改
|
||||||
if in.Id > 0 {
|
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()
|
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, append(oldTypes, in.Type))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,18 +75,45 @@ func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err
|
|||||||
if in.Type == "" {
|
if in.Type == "" {
|
||||||
return gerror.Wrap(err, "类型选择无效,请检查")
|
return gerror.Wrap(err, "类型选择无效,请检查")
|
||||||
}
|
}
|
||||||
|
if in.Type == consts.ImInternalSecretDictType {
|
||||||
|
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||||
|
}
|
||||||
|
|
||||||
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
|
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gerror.Wrap(err, consts.ErrorORM)
|
err = gerror.Wrap(err, consts.ErrorORM)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.publishDictUpdated(ctx, []string{in.Type})
|
||||||
return nil
|
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 获取列表
|
// List 获取列表
|
||||||
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
|
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
|
// 类型ID
|
||||||
if in.TypeID > 0 {
|
if in.TypeID > 0 {
|
||||||
types, err := s.GetTypes(ctx, in.TypeID)
|
types, err := s.GetTypes(ctx, in.TypeID)
|
||||||
@@ -151,6 +193,9 @@ func (s *sSysDictData) GetTypes(ctx context.Context, id int64) (types []string,
|
|||||||
|
|
||||||
// Select 获取列表
|
// Select 获取列表
|
||||||
func (s *sSysDictData) Select(ctx context.Context, in *sysin.DataSelectInp) (list sysin.DataSelectModel, err error) {
|
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)
|
options, err := dict.GetOptions(ctx, in.Type)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func init() {
|
|||||||
// Tree 树
|
// Tree 树
|
||||||
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
|
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
|
||||||
var (
|
var (
|
||||||
mod = dao.SysDictType.Ctx(ctx)
|
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||||
models []*entity.SysDictType
|
models []*entity.SysDictType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +55,9 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
|||||||
err = gerror.New("数据不存在或已删除!")
|
err = gerror.New("数据不存在或已删除!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if models.Type == consts.ImInternalSecretDictType {
|
||||||
|
return gerror.New("内部密钥字典禁止通过通用接口操作")
|
||||||
|
}
|
||||||
|
|
||||||
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
|
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,6 +86,18 @@ func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp)
|
|||||||
|
|
||||||
// Edit 修改/新增
|
// Edit 修改/新增
|
||||||
func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err error) {
|
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 {
|
if err = hgorm.IsUnique(ctx, &dao.SysDictType, g.Map{dao.SysDictType.Columns().Name: in.Name}, "名称已存在", in.Id); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -105,7 +120,7 @@ func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err
|
|||||||
// TreeSelect 获取类型关系树选项
|
// TreeSelect 获取类型关系树选项
|
||||||
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
|
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
|
||||||
var (
|
var (
|
||||||
mod = dao.SysDictType.Ctx(ctx)
|
mod = dao.SysDictType.Ctx(ctx).WhereNot(dao.SysDictType.Columns().Type, consts.ImInternalSecretDictType)
|
||||||
models []*entity.SysDictType
|
models []*entity.SysDictType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
"hotgo/internal/library/imadmin"
|
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
@@ -18,19 +17,34 @@ import (
|
|||||||
"github.com/gogf/gf/v2/net/ghttp"
|
"github.com/gogf/gf/v2/net/ghttp"
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type imAdminScope struct {
|
||||||
|
TenantId int64
|
||||||
|
OperatorId int64
|
||||||
|
RoleKey string
|
||||||
|
RequestId string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
imAdminOperationAuditTable = "im_admin_operation_audit"
|
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)
|
operatorId := contexts.GetUserId(ctx)
|
||||||
scope.OperatorId = operatorId
|
scope.OperatorId = operatorId
|
||||||
scope.RoleKey = contexts.GetRoleKey(ctx)
|
scope.RoleKey = contexts.GetRoleKey(ctx)
|
||||||
scope.RequestId = gctx.CtxId(ctx)
|
scope.RequestId = gctx.CtxId(ctx)
|
||||||
if isImPlatformAdmin(ctx, operatorId) {
|
if isImPlatformAdmin(ctx, operatorId) {
|
||||||
if selectedTenantId <= 0 {
|
if selectedTenantId <= 0 {
|
||||||
|
if allowAll {
|
||||||
|
return scope, nil
|
||||||
|
}
|
||||||
return scope, gerror.New("请选择IM租户")
|
return scope, gerror.New("请选择IM租户")
|
||||||
}
|
}
|
||||||
if err = ensureImTenantActive(ctx, selectedTenantId); err != nil {
|
if err = ensureImTenantActive(ctx, selectedTenantId); err != nil {
|
||||||
@@ -40,7 +54,7 @@ func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope ima
|
|||||||
return scope, nil
|
return scope, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !contexts.IsTenantDept(ctx) {
|
if !contexts.IsTenantDept(ctx) && !(contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) > 0) {
|
||||||
return scope, gerror.New("当前账号无权管理IM租户")
|
return scope, gerror.New("当前账号无权管理IM租户")
|
||||||
}
|
}
|
||||||
tenantId, err := resolveImAdminBoundTenant(ctx)
|
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 {
|
func isImPlatformAdmin(ctx context.Context, operatorId int64) bool {
|
||||||
if contexts.IsCompanyDept(ctx) {
|
if service.AdminMember().VerifySuperId(ctx, operatorId) {
|
||||||
return true
|
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) {
|
func resolveImAdminBoundTenant(ctx context.Context) (int64, error) {
|
||||||
@@ -69,9 +86,46 @@ func resolveImAdminBoundTenant(ctx context.Context) (int64, error) {
|
|||||||
return tenantId, nil
|
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) {
|
func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) {
|
||||||
var tenant *entity.ImTenant
|
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租户失败")
|
return gerror.Wrap(err, "获取IM租户失败")
|
||||||
}
|
}
|
||||||
if tenant == nil || tenant.Id <= 0 {
|
if tenant == nil || tenant.Id <= 0 {
|
||||||
|
|||||||
@@ -5,15 +5,25 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
|
_ "hotgo/internal/logic/admin"
|
||||||
"hotgo/internal/model"
|
"hotgo/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompanyDepartmentIsImPlatformAdmin(t *testing.T) {
|
func TestBoundCompanyDepartmentUsesBoundTenant(t *testing.T) {
|
||||||
ctx := context.WithValue(context.Background(), consts.ContextHTTPKey, &model.Context{
|
ctx := context.WithValue(context.Background(), consts.ContextHTTPKey, &model.Context{
|
||||||
User: &model.Identity{Id: 14, TenantId: 1, DeptType: consts.DeptTypeCompany, RoleKey: "imManage"},
|
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) {
|
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,
|
want: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "company department with tenant binding",
|
||||||
|
identity: &model.Identity{
|
||||||
|
TenantId: 1,
|
||||||
|
DeptType: consts.DeptTypeCompany,
|
||||||
|
},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -10,18 +10,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
"hotgo/internal/library/imadmin"
|
"hotgo/internal/library/imadmin"
|
||||||
"hotgo/internal/library/imkafka"
|
"hotgo/internal/library/imkafka"
|
||||||
"hotgo/internal/library/imtenant"
|
|
||||||
"hotgo/internal/model/entity"
|
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sSysImDeadLetter struct{}
|
type sSysImDeadLetter struct{}
|
||||||
@@ -39,7 +34,7 @@ func (s *sSysImDeadLetter) List(ctx context.Context, in *sysin.ImDeadLetterListI
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,7 +76,7 @@ func (s *sSysImDeadLetter) View(ctx context.Context, in *sysin.ImDeadLetterViewI
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -120,7 +115,7 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I
|
|||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -162,22 +157,6 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I
|
|||||||
}, nil
|
}, 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 {
|
func convertDeadLetterModel(in *sysin.ImDeadLetterListModelRaw) *sysin.ImDeadLetterListModel {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return &sysin.ImDeadLetterListModel{}
|
return &sysin.ImDeadLetterListModel{}
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import (
|
|||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
|
"hotgo/internal/library/immoderation"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sSysImExternalConfig struct{}
|
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 {
|
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if in.Service == consts.ImExternalServiceModeration && tenant.Code != "default" {
|
||||||
|
return nil, 0, gerror.New("第一阶段仅默认商户可配置内容审核服务")
|
||||||
|
}
|
||||||
mod := s.model(ctx).Where("tenant_id", tenant.Id)
|
mod := s.model(ctx).Where("tenant_id", tenant.Id)
|
||||||
|
if tenant.Code != "default" {
|
||||||
|
mod = mod.WhereNot("service", consts.ImExternalServiceModeration)
|
||||||
|
}
|
||||||
if in.Service != "" {
|
if in.Service != "" {
|
||||||
mod = mod.Where("service", 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 {
|
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||||
return nil, err
|
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 {
|
if err = s.model(ctx).Where("tenant_id", tenant.Id).Where("service", in.Service).Scan(&res); err != nil {
|
||||||
return nil, gerror.Wrap(err, "获取IM外部服务配置详情失败,请稍后重试!")
|
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 {
|
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||||
return err
|
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 := "{}"
|
configJson := "{}"
|
||||||
if in.ConfigJson != nil && in.ConfigJson.String() != "" && in.ConfigJson.String() != "null" {
|
if in.ConfigJson != nil && in.ConfigJson.String() != "" && in.ConfigJson.String() != "null" {
|
||||||
configJson = in.ConfigJson.String()
|
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() != "{}" {
|
if in.SecretJson != nil && in.SecretJson.String() != "" && in.SecretJson.String() != "null" && in.SecretJson.String() != "{}" {
|
||||||
data["secret_json"] = 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).
|
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 {
|
Where("tenant_id", tenant.Id).Where("service", in.Service).Data(data).Update(); err != nil {
|
||||||
return gerror.Wrap(err, "更新IM外部服务配置失败,请稍后重试!")
|
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{
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_external_config_update", "im_external_config", 0, "success", g.Map{
|
||||||
"service": in.Service, "provider": in.Provider, "enabled": in.Enabled,
|
"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
|
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 {
|
if err = s.ensureRows(ctx, tenant.Id); err != nil {
|
||||||
return err
|
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
|
var row gdb.Record
|
||||||
if row, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
if row, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||||
Where("tenant_id", tenant.Id).Where("service", in.Service).One(); err != nil {
|
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() == "" {
|
if row == nil || row["enabled"].Int() != 1 || row["provider"].String() == "" {
|
||||||
status = consts.ImExternalConfigStatusUnconfigured
|
status = consts.ImExternalConfigStatusUnconfigured
|
||||||
result = "未启用或服务商为空"
|
result = "未启用或服务商为空"
|
||||||
|
} else if in.Service == consts.ImExternalServiceModeration {
|
||||||
|
status, result = s.checkModeration(ctx, row)
|
||||||
}
|
}
|
||||||
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
if _, err = imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).
|
||||||
Where("tenant_id", tenant.Id).Where("service", in.Service).
|
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
|
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 {
|
func (s *sSysImExternalConfig) model(ctx context.Context) *gdb.Model {
|
||||||
return imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).Fields(
|
return imtenant.RegistryDB().Model("im_external_config").Safe().Ctx(ctx).Fields(
|
||||||
"tenant_id",
|
"tenant_id",
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hotgo/internal/library/imworkbench"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"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/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const imFeedbackConfigTable = "im_feedback_config"
|
|
||||||
|
|
||||||
type sSysImFeedbackConfig struct{}
|
type sSysImFeedbackConfig struct{}
|
||||||
|
|
||||||
func NewSysImFeedbackConfig() *sSysImFeedbackConfig { return &sSysImFeedbackConfig{} }
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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 {
|
if res != nil {
|
||||||
res.TenantId = tenant.Id
|
res.TenantId = tenant.Id
|
||||||
res.TenantName = tenant.Name
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = s.ensureDefault(ctx, db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
categories := `["bug","suggestion","complaint","other"]`
|
categories := `["bug","suggestion","complaint","other"]`
|
||||||
if in.Categories != nil && in.Categories.String() != "" && in.Categories.String() != "null" {
|
if in.Categories != nil && in.Categories.String() != "" && in.Categories.String() != "null" {
|
||||||
categories = in.Categories.String()
|
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" {
|
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||||
attachmentPolicy = in.AttachmentPolicy.String()
|
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,
|
"categories": categories,
|
||||||
"max_length": in.MaxLength,
|
"max_length": in.MaxLength,
|
||||||
"attachment_policy": attachmentPolicy,
|
"attachment_policy": attachmentPolicy,
|
||||||
"allow_contact": in.AllowContact,
|
"allow_contact": in.AllowContact,
|
||||||
"diagnostics_notice": in.DiagnosticsNotice,
|
"diagnostics_notice": in.DiagnosticsNotice,
|
||||||
"updated_at": gtime.Now(),
|
}, nil); err != nil {
|
||||||
}).Update(); err != nil {
|
return err
|
||||||
return gerror.Wrap(err, "更新IM意见反馈配置失败,请稍后重试!")
|
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
|
||||||
return nil
|
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)
|
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
|
||||||
var item imInviteCodeResponse
|
var item imInviteCodeResponse
|
||||||
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
|
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 {
|
if createErr != nil {
|
||||||
err = createErr
|
err = createErr
|
||||||
} else if len(codes) > 0 {
|
} else if len(codes) > 0 {
|
||||||
@@ -150,15 +150,27 @@ func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchC
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
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"
|
result := "success"
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = "failed"
|
result = "failed"
|
||||||
} else {
|
} else {
|
||||||
res = convertInviteBatch(batch)
|
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
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +370,7 @@ func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int
|
|||||||
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
|
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 existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
|
||||||
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
||||||
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
||||||
@@ -402,11 +414,16 @@ func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int6
|
|||||||
}
|
}
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_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)
|
(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); txErr != nil {
|
VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now, boundUserID, boundUserLabel); txErr != nil {
|
||||||
return txErr
|
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
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandL
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
where, args := buildImKafkaCommandWhere(in)
|
// 强制租户范围:商户绑定账号忽略前端所选租户,平台管理员使用所选租户
|
||||||
|
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
where, args := buildImKafkaCommandWhere(in, scope.TenantId)
|
||||||
db := imtenant.RegistryDB()
|
db := imtenant.RegistryDB()
|
||||||
totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...)
|
totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,16 +96,16 @@ func (s *sSysImKafkaCommand) View(ctx context.Context, in *sysin.ImKafkaCommandV
|
|||||||
if err = row.Struct(&res); err != nil {
|
if err = row.Struct(&res); err != nil {
|
||||||
return nil, gerror.Wrap(err, "解析IM后台命令详情失败,请稍后重试!")
|
return nil, gerror.Wrap(err, "解析IM后台命令详情失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
|
// 后置校验记录归属租户是否在操作者允许范围内
|
||||||
|
if err = ensureImRecordTenantAllowed(ctx, res.TenantId); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp) (string, []any) {
|
func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp, scopeTenantId int64) (string, []any) {
|
||||||
parts := []string{"1=1"}
|
parts := []string{"tenant_id=?"}
|
||||||
args := []any{}
|
args := []any{scopeTenantId}
|
||||||
if in.TenantId > 0 {
|
|
||||||
parts = append(parts, "tenant_id=?")
|
|
||||||
args = append(args, in.TenantId)
|
|
||||||
}
|
|
||||||
if in.CommandId != "" {
|
if in.CommandId != "" {
|
||||||
parts = append(parts, "command_id LIKE ?")
|
parts = append(parts, "command_id LIKE ?")
|
||||||
args = append(args, "%"+in.CommandId+"%")
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
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)
|
user := contexts.GetUser(ctx)
|
||||||
actor := &imkafka.Actor{
|
actor := &imkafka.Actor{
|
||||||
Type: "admin",
|
Type: "admin",
|
||||||
@@ -170,6 +183,14 @@ func (s *sSysImKafkaCommand) WorkbenchEvent(ctx context.Context, in *sysin.ImKaf
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
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)
|
user := contexts.GetUser(ctx)
|
||||||
actor := &imkafka.Actor{
|
actor := &imkafka.Actor{
|
||||||
Type: "admin",
|
Type: "admin",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
|
"hotgo/internal/library/imadmin"
|
||||||
"hotgo/internal/library/imkafka"
|
"hotgo/internal/library/imkafka"
|
||||||
"hotgo/internal/model/input/form"
|
"hotgo/internal/model/input/form"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"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) {
|
func (s *sSysImLoginFailureLog) BanUser(ctx context.Context, in *sysin.ImLoginFailureBanUserInp) (*sysin.ImUserCommandModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"hotgo/internal/library/imworkbench"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"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/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const imMailboxConfigTable = "im_mailbox_config"
|
|
||||||
|
|
||||||
type sSysImMailboxConfig struct{}
|
type sSysImMailboxConfig struct{}
|
||||||
|
|
||||||
func NewSysImMailboxConfig() *sSysImMailboxConfig { return &sSysImMailboxConfig{} }
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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 {
|
if res != nil {
|
||||||
res.TenantId = tenant.Id
|
res.TenantId = tenant.Id
|
||||||
res.TenantName = tenant.Name
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = s.ensureDefault(ctx, db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
requiredFields := `["subject","content"]`
|
requiredFields := `["subject","content"]`
|
||||||
if in.RequiredFields != nil && in.RequiredFields.String() != "" && in.RequiredFields.String() != "null" {
|
if in.RequiredFields != nil && in.RequiredFields.String() != "" && in.RequiredFields.String() != "null" {
|
||||||
requiredFields = in.RequiredFields.String()
|
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" {
|
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
|
||||||
attachmentPolicy = in.AttachmentPolicy.String()
|
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,
|
"enabled": in.Enabled,
|
||||||
"allow_anonymous": in.AllowAnonymous,
|
"allow_anonymous": in.AllowAnonymous,
|
||||||
"required_fields": requiredFields,
|
"required_fields": requiredFields,
|
||||||
@@ -69,53 +60,9 @@ func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxCon
|
|||||||
"attachment_policy": attachmentPolicy,
|
"attachment_policy": attachmentPolicy,
|
||||||
"notice_title": in.NoticeTitle,
|
"notice_title": in.NoticeTitle,
|
||||||
"notice_content": in.NoticeContent,
|
"notice_content": in.NoticeContent,
|
||||||
"notice_updated_at": gtime.Now(),
|
}, nil); err != nil {
|
||||||
"updated_at": gtime.Now(),
|
return err
|
||||||
}).Update(); err != nil {
|
|
||||||
return gerror.Wrap(err, "更新IM总经理信箱配置失败,请稍后重试!")
|
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
|
||||||
return nil
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId)
|
tenantID, tenantCode, err := resolveImPolicyTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,11 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
|||||||
}
|
}
|
||||||
version = uint(maxValue) + 1
|
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 in.Id > 0 {
|
||||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil {
|
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil {
|
||||||
return updateErr
|
return updateErr
|
||||||
@@ -158,6 +162,20 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
|||||||
return id, nil
|
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 {
|
func (s *sSysImSecurityPolicy) Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) error {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -238,6 +256,90 @@ func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImS
|
|||||||
return id, err
|
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) {
|
func (s *sSysImSecurityPolicy) IPRuleList(ctx context.Context, in *sysin.ImSecurityIPRuleListInp) (list []*sysin.ImSecurityIPRuleModel, total int, err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ import (
|
|||||||
"hotgo/internal/library/hgorm/hook"
|
"hotgo/internal/library/hgorm/hook"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
|
"hotgo/internal/model/input/adminin"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
"hotgo/utility/tree"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
@@ -25,6 +27,28 @@ import (
|
|||||||
|
|
||||||
type sSysImTenant struct{}
|
type sSysImTenant struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
|
||||||
|
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
|
||||||
|
tenantConfigMessageEncryptionEnabled = "message_encryption_enabled"
|
||||||
|
tenantConfigRealnameReviewPolicy = "realname_review_policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// imManageRoleKey 商户级IM管理员角色标识
|
||||||
|
const imManageRoleKey = "imManage"
|
||||||
|
|
||||||
|
type imTenantConfigModeration struct {
|
||||||
|
Id int64 `json:"id" orm:"id"`
|
||||||
|
TenantId int64 `json:"tenant_id" orm:"tenant_id"`
|
||||||
|
MaxUsers uint64 `json:"max_users" orm:"max_users"`
|
||||||
|
MaxGroups uint64 `json:"max_groups" orm:"max_groups"`
|
||||||
|
MaxGroupMembers uint `json:"max_group_members" orm:"max_group_members"`
|
||||||
|
AliyunModerationEnabled int `json:"aliyun_moderation_enabled" orm:"aliyun_moderation_enabled"`
|
||||||
|
LocalModerationEnabled int `json:"local_moderation_enabled" orm:"local_moderation_enabled"`
|
||||||
|
MessageEncryptionEnabled int `json:"message_encryption_enabled" orm:"message_encryption_enabled"`
|
||||||
|
RealnameReviewPolicy int `json:"realname_review_policy" orm:"realname_review_policy"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewSysImTenant() *sSysImTenant {
|
func NewSysImTenant() *sSysImTenant {
|
||||||
return &sSysImTenant{}
|
return &sSysImTenant{}
|
||||||
}
|
}
|
||||||
@@ -61,6 +85,14 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis
|
|||||||
dao.ImTenant.Columns().UpdatedAt,
|
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 {
|
if in.Id > 0 {
|
||||||
mod = mod.Where(dao.ImTenant.Columns().Id, in.Id)
|
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 != "" {
|
if in.Name != "" {
|
||||||
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+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+"%")
|
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
|
||||||
}
|
}
|
||||||
if in.State != nil {
|
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 {
|
if err = s.fillListConfig(ctx, list); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 业务库名仅超管可见,非超管脱敏返回
|
||||||
|
if !isSuperTenantAdmin(ctx) {
|
||||||
|
for _, item := range list {
|
||||||
|
item.DbName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +142,13 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
|
|||||||
ids = append(ids, item.Id)
|
ids = append(ids, item.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configs []*entity.ImTenantConfig
|
var configs []*imTenantConfigModeration
|
||||||
if err = s.configModel(ctx).
|
if err = s.configModel(ctx).
|
||||||
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
|
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
|
||||||
Scan(&configs); err != nil {
|
Scan(&configs); err != nil {
|
||||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
configMap := make(map[int64]*entity.ImTenantConfig, len(configs))
|
configMap := make(map[int64]*imTenantConfigModeration, len(configs))
|
||||||
for _, item := range configs {
|
for _, item := range configs {
|
||||||
configMap[item.TenantId] = item
|
configMap[item.TenantId] = item
|
||||||
}
|
}
|
||||||
@@ -127,24 +165,71 @@ func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenan
|
|||||||
dsnMap[item.Id] = item.DbDsn
|
dsnMap[item.Id] = item.DbDsn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super := isSuperTenantAdmin(ctx)
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
if conf, ok := configMap[item.Id]; ok {
|
if conf, ok := configMap[item.Id]; ok {
|
||||||
item.MaxUsers = conf.MaxUsers
|
item.MaxUsers = conf.MaxUsers
|
||||||
item.MaxGroups = conf.MaxGroups
|
item.MaxGroups = conf.MaxGroups
|
||||||
item.MaxGroupMembers = conf.MaxGroupMembers
|
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
|
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) {
|
func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
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 {
|
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -152,22 +237,168 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err
|
|||||||
userId := contexts.GetUserId(ctx)
|
userId := contexts.GetUserId(ctx)
|
||||||
if in.Id > 0 {
|
if in.Id > 0 {
|
||||||
in.UpdatedBy = userId
|
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).
|
if _, err = s.Model(ctx).
|
||||||
Fields(sysin.ImTenantUpdateFields{}).
|
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
|
||||||
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
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.CreatedBy = userId
|
||||||
|
in.UpdatedBy = userId
|
||||||
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||||
Fields(sysin.ImTenantInsertFields{}).
|
Fields(sysin.ImTenantInsertFields{}).
|
||||||
Data(in).InsertAndGetId()
|
Data(in).InsertAndGetId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
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
|
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
|
var conf *entity.ImTenantConfig
|
||||||
if err = s.configModel(ctx).
|
if err = s.configModel(ctx).
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
|
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||||
Scan(&conf); err != nil {
|
Scan(&conf); err != nil {
|
||||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := g.Map{
|
data := g.Map{
|
||||||
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
|
dao.ImTenantConfig.Columns().MaxUsers: in.MaxUsers,
|
||||||
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
|
dao.ImTenantConfig.Columns().MaxGroups: in.MaxGroups,
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
|
dao.ImTenantConfig.Columns().MaxGroupMembers: in.MaxGroupMembers,
|
||||||
|
tenantConfigAliyunModerationEnabled: in.AliyunModerationEnabled,
|
||||||
|
tenantConfigLocalModerationEnabled: in.LocalModerationEnabled,
|
||||||
|
tenantConfigMessageEncryptionEnabled: in.MessageEncryptionEnabled,
|
||||||
|
tenantConfigRealnameReviewPolicy: in.RealnameReviewPolicy,
|
||||||
}
|
}
|
||||||
if conf != nil && conf.Id > 0 {
|
if conf != nil && conf.Id > 0 {
|
||||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
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().MaxUsers,
|
||||||
dao.ImTenantConfig.Columns().MaxGroups,
|
dao.ImTenantConfig.Columns().MaxGroups,
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||||
|
tenantConfigAliyunModerationEnabled,
|
||||||
|
tenantConfigLocalModerationEnabled,
|
||||||
|
tenantConfigMessageEncryptionEnabled,
|
||||||
|
tenantConfigRealnameReviewPolicy,
|
||||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
dao.ImTenantConfig.Columns().UpdatedBy,
|
||||||
).
|
).
|
||||||
WherePri(conf.Id).Data(data).Update(); err != nil {
|
WherePri(conf.Id).Data(data).Update(); err != nil {
|
||||||
@@ -214,15 +465,21 @@ func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
|
data[dao.ImTenantConfig.Columns().TenantId] = in.Id
|
||||||
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
|
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
|
||||||
|
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
||||||
if _, err = s.configModel(ctx).
|
if _, err = s.configModel(ctx).
|
||||||
Fields(
|
Fields(
|
||||||
dao.ImTenantConfig.Columns().TenantId,
|
dao.ImTenantConfig.Columns().TenantId,
|
||||||
dao.ImTenantConfig.Columns().MaxUsers,
|
dao.ImTenantConfig.Columns().MaxUsers,
|
||||||
dao.ImTenantConfig.Columns().MaxGroups,
|
dao.ImTenantConfig.Columns().MaxGroups,
|
||||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||||
|
tenantConfigAliyunModerationEnabled,
|
||||||
|
tenantConfigLocalModerationEnabled,
|
||||||
|
tenantConfigMessageEncryptionEnabled,
|
||||||
|
tenantConfigRealnameReviewPolicy,
|
||||||
dao.ImTenantConfig.Columns().CreatedBy,
|
dao.ImTenantConfig.Columns().CreatedBy,
|
||||||
|
dao.ImTenantConfig.Columns().UpdatedBy,
|
||||||
).
|
).
|
||||||
Data(data).Insert(); err != nil {
|
Data(data).Insert(); err != nil {
|
||||||
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
|
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
|
||||||
@@ -243,7 +500,7 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var conf *entity.ImTenantConfig
|
var conf *imTenantConfigModeration
|
||||||
if err = s.configModel(ctx).
|
if err = s.configModel(ctx).
|
||||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||||
Scan(&conf); err != nil {
|
Scan(&conf); err != nil {
|
||||||
@@ -254,8 +511,37 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res
|
|||||||
res.MaxUsers = conf.MaxUsers
|
res.MaxUsers = conf.MaxUsers
|
||||||
res.MaxGroups = conf.MaxGroups
|
res.MaxGroups = conf.MaxGroups
|
||||||
res.MaxGroupMembers = conf.MaxGroupMembers
|
res.MaxGroupMembers = conf.MaxGroupMembers
|
||||||
|
res.AliyunModerationEnabled = conf.AliyunModerationEnabled
|
||||||
|
res.LocalModerationEnabled = conf.LocalModerationEnabled
|
||||||
|
res.MessageEncryptionEnabled = conf.MessageEncryptionEnabled
|
||||||
|
res.RealnameReviewPolicy = conf.RealnameReviewPolicy
|
||||||
}
|
}
|
||||||
res.DbDsnConfigured = res.DbDsn != ""
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +581,11 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r
|
|||||||
DbDsnConfigured: tenant.DbDsn != "",
|
DbDsnConfigured: tenant.DbDsn != "",
|
||||||
Warnings: []string{},
|
Warnings: []string{},
|
||||||
}
|
}
|
||||||
|
// 业务库名与连接串信息仅超管可见,非超管全部脱敏
|
||||||
|
if !isSuperTenantAdmin(ctx) {
|
||||||
|
res.DbName = ""
|
||||||
|
res.DbDsnConfigured = false
|
||||||
|
}
|
||||||
|
|
||||||
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
|
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
|
||||||
if dbErr != nil {
|
if dbErr != nil {
|
||||||
|
|||||||
@@ -206,8 +206,13 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
|||||||
values.Set("user_ids", strings.Join(userIds, ","))
|
values.Set("user_ids", strings.Join(userIds, ","))
|
||||||
}
|
}
|
||||||
if in.UserId != "" {
|
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)
|
values.Set("account_no", in.UserId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if in.Nickname != "" {
|
if in.Nickname != "" {
|
||||||
values.Set("nickname", 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.public_account",
|
||||||
"up.gender",
|
"up.gender",
|
||||||
"up.region",
|
"up.region",
|
||||||
|
"COALESCE(up.friend_add_enabled, 1) friend_add_enabled",
|
||||||
"us.auth_version",
|
"us.auth_version",
|
||||||
"us.password_updated_at",
|
"us.password_updated_at",
|
||||||
)
|
)
|
||||||
@@ -589,6 +595,54 @@ func (s *sSysImUser) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGrou
|
|||||||
return nil
|
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) {
|
func (s *sSysImUser) IPWhitelist(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (*sysin.ImUserIPWhitelistModel, error) {
|
||||||
if err := in.Filter(ctx); err != nil {
|
if err := in.Filter(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,18 +2,15 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/library/imworkbench"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const imWorkbenchAppsTable = "im_workbench_apps"
|
|
||||||
|
|
||||||
type sSysImWorkbench struct{}
|
type sSysImWorkbench struct{}
|
||||||
|
|
||||||
func NewSysImWorkbench() *sSysImWorkbench { return &sSysImWorkbench{} }
|
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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
values := url.Values{}
|
||||||
return
|
values.Set("page", intToString(in.Page))
|
||||||
}
|
values.Set("per_page", intToString(in.PerPage))
|
||||||
mod := s.model(ctx, db)
|
|
||||||
if in.Code != "" {
|
if in.Code != "" {
|
||||||
mod = mod.WhereLike("code", "%"+in.Code+"%")
|
values.Set("code", in.Code)
|
||||||
}
|
}
|
||||||
if in.Name != "" {
|
if in.Name != "" {
|
||||||
mod = mod.WhereLike("name", "%"+in.Name+"%")
|
values.Set("name", in.Name)
|
||||||
}
|
}
|
||||||
if in.Enabled != nil {
|
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")
|
var page imworkbench.Page[sysin.ImWorkbenchAppModel]
|
||||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
if err = imworkbench.DefaultClient(ctx).Get(ctx, "/internal/admin/workbench/apps", values, &page); err != nil {
|
||||||
err = gerror.Wrap(err, "获取IM工作台模块失败,请稍后重试!")
|
return nil, 0, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
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.TenantId = tenant.Id
|
||||||
item.TenantName = tenant.Name
|
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) {
|
func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error) {
|
||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := g.Map{
|
data := g.Map{
|
||||||
|
"id": in.Id,
|
||||||
"code": in.Code,
|
"code": in.Code,
|
||||||
"name": in.Name,
|
"name": in.Name,
|
||||||
"icon_file_id": in.IconFileId,
|
"icon_file_id": in.IconFileId,
|
||||||
@@ -75,25 +71,18 @@ func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEdit
|
|||||||
"unread_key": in.UnreadKey,
|
"unread_key": in.UnreadKey,
|
||||||
"enabled": in.Enabled,
|
"enabled": in.Enabled,
|
||||||
"permission": in.Permission,
|
"permission": in.Permission,
|
||||||
"updated_at": gtime.Now(),
|
|
||||||
}
|
}
|
||||||
action := "im_workbench_app_create"
|
action := "im_workbench_app_create"
|
||||||
targetId := in.Id
|
targetId := in.Id
|
||||||
if in.Id > 0 {
|
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"
|
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()
|
var res sysin.ImWorkbenchAppModel
|
||||||
if err != nil {
|
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps", data, &res); err != nil {
|
||||||
return gerror.Wrap(err, "获取IM工作台模块ID失败,请稍后重试!")
|
return err
|
||||||
}
|
}
|
||||||
targetId = idValue.Int64()
|
if targetId <= 0 {
|
||||||
|
targetId = res.Id
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_workbench_app", targetId, "success", g.Map{
|
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_workbench_app", targetId, "success", g.Map{
|
||||||
"code": in.Code, "route": in.Route, "enabled": in.Enabled, "permission": in.Permission,
|
"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 {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
if err = imworkbench.DefaultClient(ctx).Post(ctx, "/internal/admin/workbench/apps/disable", g.Map{"id": in.Id, "code": in.Code}, nil); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
|
||||||
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工作台模块失败,请稍后重试!")
|
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
|
||||||
return nil
|
return nil
|
||||||
@@ -129,66 +109,23 @@ func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppR
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
|
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = s.ensureDefaultApps(ctx, db); err != nil {
|
items := make([]g.Map, 0, len(in.Items))
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
||||||
for _, item := range in.Items {
|
for _, item := range in.Items {
|
||||||
if _, e := tx.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item.Code).
|
items = append(items, g.Map{
|
||||||
Data(g.Map{"sort_order": item.SortOrder, "updated_at": gtime.Now()}).Update(); e != nil {
|
"code": item.Code,
|
||||||
return e
|
"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
|
return err
|
||||||
}); err != nil {
|
|
||||||
return gerror.Wrap(err, "调整IM工作台模块排序失败,请稍后重试!")
|
|
||||||
}
|
}
|
||||||
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_reorder", "im_workbench_app", 0, "success", g.Map{"count": len(in.Items)})
|
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_reorder", "im_workbench_app", 0, "success", g.Map{"count": len(in.Items)})
|
||||||
return nil
|
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:"到期时间"`
|
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
||||||
Remark string `json:"remark" dc:"备注"`
|
Remark string `json:"remark" dc:"备注"`
|
||||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||||
|
UserId int64 `json:"userId" dc:"绑定C端用户ID,非必填,>0时为批次内所有邀请码绑定该用户"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
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 {
|
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
|
||||||
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
||||||
}
|
}
|
||||||
|
if in.UserId < 0 {
|
||||||
|
return gerror.New("绑定用户ID不合法")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user