Compare commits

...
86 Commits
Author SHA1 Message Date
richjohnrambo f56b84bb10 fix: display finance records and ledgers in Beijing time
Deploy olons-server production / deploy (push) Successful in 34s
2026-10-23 00:13:07 +08:00
richjohnrambo 8ce67648b1 feat: add global system notices and Beijing order time display
Deploy olons-server production / deploy (push) Successful in 3m12s
2026-10-22 21:56:07 +08:00
richjohnrambo 9bb479b506 feat: use floating customer service widget in pro
Deploy olons-server production / deploy (push) Successful in 3m1s
2026-10-21 13:15:15 +08:00
richjohnrambo 79c502cb20 fix: exclude test accounts from leaderboard
Deploy olons-server production / deploy (push) Successful in 4m29s
2026-10-20 23:35:17 +08:00
richjohnrambo 47b545d050 feat: directly add customer service friend
Deploy olons-server production / deploy (push) Successful in 3m26s
2026-10-20 22:24:56 +08:00
richjohnrambo 78165b7a15 feat: use login popup announcement on home
Deploy olons-server production / deploy (push) Successful in 4m57s
2026-10-20 22:13:51 +08:00
Your Name 6954d375b5 fix: select support identity by IM tenant
Deploy olons-server production / deploy (push) Successful in 5m4s
2026-10-20 21:42:43 +08:00
Your Name 975a2a2ed2 fix: preserve pending support state on private profiles
Deploy olons-server production / deploy (push) Successful in 2m42s
2026-10-20 21:31:44 +08:00
Your Name 5f85472757 fix: handle private pending support relationships
Deploy olons-server production / deploy (push) Successful in 3m23s
2026-10-20 21:25:19 +08:00
richjohnrambo 92b5656f3f fix: reprovision IM member identities across tenants
Deploy olons-server production / deploy (push) Successful in 2m40s
2026-10-20 20:55:10 +08:00
Your Name cdebd0a404 fix(ci): deploy olons server from managed compose env
Deploy olons-server production / deploy (push) Successful in 32s
2026-10-20 17:49:03 +08:00
richjohnrambo ba87d70478 fix: show customer chat before connection completes
Deploy olons-server production / deploy (push) Successful in 4m30s
2026-10-19 21:32:56 +08:00
Your Name db1367b8fe fix: render inline support images
Deploy olons-server production / deploy (push) Successful in 4m1s
2026-10-19 18:06:00 +08:00
Your Name db18514e6d fix: make support IM delivery recoverable
Deploy olons-server production / deploy (push) Successful in 3m38s
2026-10-19 17:23:34 +08:00
Your Name c39ec78e89 docs: design support IM reliability fix 2026-10-19 16:50:29 +08:00
richjohnrambo e6c421ac3e fix: inject production IM API key
Deploy olons-server production / deploy (push) Successful in 37s
2026-10-19 13:33:10 +08:00
richjohnrambo 74785f948b fix: use personal login for default IM tenant
Deploy olons-server production / deploy (push) Successful in 2m48s
2026-10-19 13:25:32 +08:00
richjohnrambo f2f4720340 chore: expose support integration errors during deploy
Deploy olons-server production / deploy (push) Successful in 1m9s
2026-10-19 13:22:44 +08:00
richjohnrambo dffd47294e fix: preserve production compose variables
Deploy olons-server production / deploy (push) Successful in 1m9s
2026-10-19 13:20:27 +08:00
richjohnrambo 950890a734 fix: override production IM endpoints
Deploy olons-server production / deploy (push) Failing after 35s
2026-10-19 13:17:59 +08:00
richjohnrambo 179a9bdd16 fix: bind Olons support to production IM
Deploy olons-server production / deploy (push) Successful in 52s
2026-10-19 13:16:10 +08:00
richjohnrambo aa0e17f1f1 fix: inject IM internal secret in production
Deploy olons-server production / deploy (push) Successful in 33s
2026-10-19 13:13:08 +08:00
richjohnrambo 47bb4fe2ed Merge source dev into pro
Deploy olons-server production / deploy (push) Successful in 1m27s
2026-10-19 11:30:39 +08:00
richjohnrambo 355d59813b fix: use default IM tenant 2026-10-19 11:28:15 +08:00
dev 04a3a0f538 Merge source dev into pro
Deploy olons-server production / deploy (push) Successful in 1m50s
2026-10-19 11:14:26 +08:00
richjohnrambo 1c11c0fa82 fix: restore payment account details 2026-10-19 11:14:09 +08:00
dev 5e0abedbcc Merge source dev into pro
Deploy olons-server production / deploy (push) Successful in 1m45s
2026-10-19 09:34:44 +08:00
richjohnrambo 21371061ed feat: improve home notice ticker 2026-10-19 09:34:26 +08:00
Your Name 319768bd82 test: align deployment contract with production
Deploy olons-server production / deploy (push) Successful in 28s
2026-10-19 02:55:32 +08:00
dev d8facff163 Merge source dev into pro
Deploy olons-server production / deploy (push) Successful in 2m29s
2026-10-19 01:03:52 +08:00
richjohnrambo 2e47dd3935 fix: remove tenant dependency from member login 2026-10-19 01:03:31 +08:00
richjohnrambo 715740fc30 Merge remote-tracking branch 'refs/remotes/origin/dev' into HEAD
Deploy olons-server production / deploy (push) Successful in 3m10s
2026-10-19 00:31:28 +08:00
richjohnrambo 804318e5f5 fix: load mall claimed counts from database 2026-10-19 00:02:35 +08:00
richjohnrambo 8b773a1a1f feat: show mall claimed counts 2026-10-18 22:52:20 +08:00
richjohnrambo 86efdf4525 feat mobile handmade mall grid 2026-10-18 21:34:27 +08:00
richjohnrambo 5a0fe67027 fix mobile task rules and feature availability 2026-10-18 20:53:33 +08:00
Your Name f9497118bb ci: deploy pro with docker compose
Deploy olons-server production / deploy (push) Successful in 3m29s
2026-10-18 20:11:07 +08:00
richjohnrambo b79b781f15 feat: add object storage and points leaderboard 2026-10-18 18:11:34 +08:00
richjohnrambo bce5edfaf5 feat: connect portal content and banner carousel 2026-10-16 18:59:16 +08:00
richjohnrambo a86db10342 fix: edit existing payment account in binding form 2026-10-16 14:58:45 +08:00
richjohnrambo 70cd0e592a fix: isolate customer service chats and reload bank details 2026-10-16 14:01:31 +08:00
richjohnrambo 443622cd3c fix: preserve legacy payment account display 2026-10-16 10:14:41 +08:00
richjohnrambo ecf1a89313 feat: show own withdrawal account details and enable avatar upload 2026-10-16 00:19:11 +08:00
richjohnrambo 07c654b6d4 fix: add close button to withdrawal confirmation modal 2026-10-15 23:08:01 +08:00
richjohnrambo 66121f48e9 fix: allow optional digital RMB code and inline payment password setup 2026-10-15 23:03:59 +08:00
richjohnrambo 6e223e537b feat: open support after recharge submission 2026-10-15 20:54:10 +08:00
richjohnrambo eb96b7ac1f feat: use six-digit numeric member invite codes 2026-10-15 18:41:35 +08:00
richjohnrambo b6a0f819b8 feat: require invitation code for registration 2026-10-15 17:31:42 +08:00
richjohnrambo 05d6ec26ad fix: require balance for automatic task claims 2026-10-15 16:40:39 +08:00
richjohnrambo acf50b9b7c feat: improve member payment and order flows 2026-10-15 15:03:39 +08:00
richjohnrambo 06eaf748ed fix: recover stale IM member credentials 2026-10-14 18:18:18 +08:00
richjohnrambo a6d7c8dda8 fix: inject IM credential into deployment 2026-10-14 18:09:55 +08:00
richjohnrambo 37913c37d1 fix: inject IM credential during deployment 2026-10-14 18:08:47 +08:00
richjohnrambo 48b3564ac9 feat: integrate customer service chat 2026-10-14 16:37:12 +08:00
richjohnrambo f3f59fe7c3 feat add customer service IM client 2026-10-14 10:08:57 +08:00
richjohnrambo dedacfcd9a feat: 完善自动与手动派单流程 2026-10-13 22:13:47 +08:00
richjohnrambo 7eedbde4ab fix: parse configured dispatch delay strings 2026-10-13 17:05:53 +08:00
richjohnrambo acbfaf97a6 fix: hide countdown for manual dispatch 2026-10-13 16:55:53 +08:00
richjohnrambo b3bf41af8b fix: preserve order dispatch mode 2026-10-13 15:45:55 +08:00
richjohnrambo 45ef10b2af feat: add scheduled automatic task matching 2026-10-13 15:21:08 +08:00
Your Name 2a3c411c9b fix: serve integrated frontend from root 2026-10-13 15:06:53 +08:00
Your Name 76421fbc2d docs: fix actions plan formatting 2026-10-13 14:36:41 +08:00
Your Name 5e01e4344b docs: plan manual actions entry 2026-10-13 14:34:39 +08:00
Your Name 4a70b85353 docs: record manual actions branch choice 2026-10-13 14:29:40 +08:00
richjohnrambo b2ebea4c80 feat: 完善移动端会员业务流程 2026-10-13 14:19:55 +08:00
richjohnrambo 32c6238751 完善C端功能、通知明细及接口文档 2026-10-13 01:30:21 +08:00
Your Name 6d6b6a0b59 fix: serve frontend assets below admin path 2026-10-13 01:09:28 +08:00
Your Name 35b6a37dad fix: include timezone data in runtime image 2026-10-13 00:57:51 +08:00
Your Name 99c55f1145 deploy: prepare olons server test rollout 2026-10-13 00:53:31 +08:00
Your Name abe697317c docs: design olons server test deployment 2026-10-12 23:45:44 +08:00
richjohnrambo 96d1de4a95 商城c端前端 2026-10-12 23:40:02 +08:00
richjohnrambo dd729cb679 商城c端前端 2026-10-12 23:26:36 +08:00
richjohnrambo 8f3a97a54d 商城c端前端 2026-10-12 20:29:27 +08:00
richjohnrambo 2d411e1b44 排除前台编译文件 2026-10-12 16:18:15 +08:00
richjohnrambo 3f15082ebc feat: add integrated web frontend 2026-10-10 23:47:28 +08:00
richjohnrambo 81e3cf1d03 feat: migrate workbench services 2026-10-10 23:34:06 +08:00
richjohnrambo c13fb72849 fix: split workbench core mysql access 2026-10-07 14:38:56 +08:00
Your Name 8d32376de8 feat: split workbench core mysql access 2026-10-06 23:44:37 +08:00
richjohnrambo 71b49ddcf7 fix: preserve meeting note file metadata 2026-09-30 14:25:44 +08:00
richjohnrambo d2e70efd9d feat: async ai workbench tasks 2026-09-30 11:37:49 +08:00
richjohnrambo 47a10c93be fix: improve ai table file handling 2026-09-30 10:02:00 +08:00
richjohnrambo 1bcce3cdde fix: fallback on invalid ai table output 2026-09-29 17:37:57 +08:00
richjohnrambo 192ab342f6 fix: upload ai table exports to oss 2026-09-29 16:50:36 +08:00
richjohnrambo 9cb8c4d93a fix: generate ai table docx exports 2026-09-29 16:40:28 +08:00
richjohnrambo 37e89a6457 fix: persist ai task user ids consistently 2026-09-29 15:51:12 +08:00
richjohnrambo 97a43a6c96 fix: load ai config from manager 2026-09-29 15:07:27 +08:00
1179 changed files with 24216 additions and 13552 deletions
Vendored
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
.git
.gitea
.idea
.vscode
.DS_Store
*.log
tests
docs
web/node_modules
web/dist
coverage
+66 -22
View File
@@ -1,37 +1,81 @@
name: Deploy im-workbench (k3s test cluster) name: Deploy olons-server production
on: on:
push:
branches:
- pro
workflow_dispatch: workflow_dispatch:
inputs:
image_tag: concurrency:
description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)' group: production-olons-server
required: false cancel-in-progress: false
permissions:
contents: read
env:
REGISTRY: git.olns.shop
IMAGE: git.olns.shop/pro/olons_server
SERVICE: olons-server
DEPLOY_HOST: 145.223.69.9
DEPLOY_USER: olns-deploy
jobs: jobs:
deploy: deploy:
runs-on: deploy-host runs-on: deploy-host
timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: 确定镜像tag - name: Log in to Gitea Container Registry
id: vars env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: | run: |
TAG="${{ github.event.inputs.image_tag }}" set -euo pipefail
[ -z "$TAG" ] && TAG=test printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: 登录镜像仓库 - name: Build and push image
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin env:
GITHUB_SHA: ${{ github.sha }}
- name: 构建并推送镜像
run: | run: |
docker build -t git.118im.com/im_team/im-workbench:${{ steps.vars.outputs.tag }} . set -euo pipefail
docker push git.118im.com/im_team/im-workbench:${{ steps.vars.outputs.tag }} SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
docker build --build-arg VITE_CUSTOMER_SERVICE_PROVIDER=third_party -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
docker push "$IMAGE:pro"
docker push "$IMAGE:$SHORT_SHA"
- name: 部署到k3s测试集群 - name: Deploy Compose service
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: | run: |
mkdir -p ~/.kube set -euo pipefail
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config install -d -m 700 "$HOME/.ssh"
chmod 600 ~/.kube/config KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")"
kubectl rollout restart deployment/im-workbench -n workbench-system cleanup() {
kubectl rollout status deployment/im-workbench -n workbench-system --timeout=120s shred -u "$KEY_FILE" 2>/dev/null || true
}
trap cleanup EXIT
printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
SSH_ARGS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=yes)
printf '%s' "$REGISTRY_TOKEN" | ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
"docker login git.olns.shop -u admin --password-stdin"
ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" '
set -euo pipefail
cd /opt/olns
docker compose pull olons-server
docker compose up -d --no-deps --wait olons-server
docker compose exec -T olons-server sh -c '"'"'
test -n "$OLONS_IM_INTERNAL_SECRET"
test "$OLONS_IM_TENANT_CODE" = "default"
test "$OLONS_SERVER_DEFAULT_TENANT_ID" = "0"
test "$OLONS_IM_BASE_URL" = "http://im-server:6060"
test "$OLONS_IM_HTTP_API_URL" = "http://im-server:6060/v1/im"
test "$OLONS_IM_WS_URL" = "ws://im-server:6060/v0/channels"
test "$OLONS_IM_FILE_URL" = "http://im-server:6060/v0/file"
'"'"'
'
+2
View File
@@ -26,6 +26,8 @@ go.work.sum
# env file # env file
.env .env
.idea/ .idea/
.DS_Store
storage/uploads/
# ---> Node # ---> Node
# Logs # Logs
+22 -10
View File
@@ -1,22 +1,34 @@
# ---- Stage 1: build the Vue frontend ----
FROM node:22-alpine AS web-builder
WORKDIR /web
RUN corepack enable
COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY web/ ./
ARG VITE_CUSTOMER_SERVICE_PROVIDER=
ENV VITE_PUBLIC_PATH=/
ENV VITE_CUSTOMER_SERVICE_PROVIDER=${VITE_CUSTOMER_SERVICE_PROVIDER}
RUN pnpm run build
# ---- Stage 2: build the Go backend ----
FROM golang:1.26 AS builder FROM golang:1.26 AS builder
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/im_workbench ./cmd/workbench COPY --from=web-builder /web/dist ./web/dist
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/olons_server ./cmd/olons_server
FROM alpine:3.22 FROM alpine:3.22
WORKDIR /app WORKDIR /app
ENV IM_WORKBENCH_CONFIG=/app/configs/workbench.test.json RUN apk add --no-cache tzdata
ENV IM_WORKBENCH_INTERNAL_SERVICE_NAME=hotgo-admin ENV OLONS_SERVER_CONFIG=/app/configs/olons_server.test.json
ENV IM_WORKBENCH_INTERNAL_SECRET=hotgo-workbench-internal COPY --from=builder /out/olons_server /app/olons_server
ENV IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com
ENV IM_WORKBENCH_MANAGER_SERVICE_NAME=im-workbench
ENV IM_CONFIG_API_SECRET=
ENV IM_INTERNAL_API_SECRET=
COPY --from=builder /out/im_workbench /app/im_workbench
COPY configs /app/configs COPY configs /app/configs
COPY --from=web-builder /web/dist /app/web/dist
ENV OLONS_SERVER_FRONTEND_DIR=/app/web/dist
EXPOSE 6061 EXPOSE 6061
ENTRYPOINT ["/app/im_workbench"] ENTRYPOINT ["/app/olons_server"]
+9 -3
View File
@@ -1,10 +1,16 @@
.PHONY: build test run .PHONY: build test run web-install web-build
build: build:
go build -o bin/im_workbench ./cmd/workbench go build -o bin/olons_server ./cmd/olons_server
test: test:
go test ./... go test ./...
run: run:
IM_WORKBENCH_CONFIG=configs/workbench.local.json go run ./cmd/workbench OLONS_SERVER_CONFIG=configs/olons_server.local.json go run ./cmd/olons_server
web-install:
cd web && pnpm install --frozen-lockfile
web-build:
cd web && pnpm run build
+57 -120
View File
@@ -1,146 +1,83 @@
# im_workbench # Olons Server
工作台服务从 `im_server` 拆分后的独立 Go 工程。当前阶段只拆服务和接口边界,数据库暂不迁移,继续访问现有租户库中的工作台表。 移动端任务与账户平台后端。项目是独立应用,业务数据以 MySQL `olons_admin` 库中的 `hg_*` 表为唯一事实源。
## 路由 项目现在采用前后端一体目录结构:`web/` 是 Vue 3 + Vite 移动端,Go 服务在生产环境通过网站根路径 `/` 提供构建后的静态资源,`/v1/` 继续提供业务 API。
## 当前接口
- `POST /v1/auth/register`
- `POST /v1/auth/login`
- `POST /v1/auth/logout`
- `GET /v1/me`
- `PATCH /v1/me/avatar`
- `PUT /v1/me/password`
- `PUT /v1/me/trade-password`
- `GET /v1/home`
- `GET /v1/banners`
- `GET /v1/contents/{content_key}`
- `GET /v1/member-levels`
- `GET /v1/task-halls`
- `GET /v1/tasks/eligibility`
- `GET /v1/tasks/stats`
- `POST /v1/task-orders/claim`
- `GET /v1/task-orders`
- `GET /v1/task-orders/{order_id}`
- `POST /v1/task-orders/{order_id}/submissions`
- `GET /v1/manual-products`
- `GET /v1/manual-products/{product_id}`
- `GET /v1/payment-accounts`
- `POST /v1/payment-accounts`
- `PUT /v1/payment-accounts`
- `GET /v1/payment-accounts/details`
- `POST /v1/withdrawal-orders/quote`
- `POST /v1/withdrawal-orders`
- `GET /v1/withdrawal-orders`
- `GET /v1/withdrawal-orders/{order_id}`
- `GET /healthz` - `GET /healthz`
- `GET /metrics` - `GET /metrics`
- `GET /v1/workbench`
- `/v1/workbench/organization/*`
- `/v1/workbench/attendance/*`
- `/v1/workbench/meetings*`
- `/v1/workbench/meeting-notes`
- `/v1/workbench/reports/daily*`
- `/v1/workbench/reports/monthly*`
- `/v1/workbench/executive-mailbox/*`
- `/internal/admin/workbench/*`,HotGo 后台内部管理接口,使用服务间 HMAC 鉴权
## 本地运行 注册使用 `hg_app_member` 作为账户主档,并在同一事务中创建 `hg_app_member_profile` 和 `hg_wallet_account` 记录。登录按完整 `mobile` 查找会员,校验 bcrypt 密码后在 Redis 中创建独立会话。
## 启动配置
核心环境变量:
```bash ```bash
IM_WORKBENCH_CONFIG=configs/workbench.local.json \ OLONS_SERVER_CONFIG=configs/olons_server.local.json
IM_WORKBENCH_INTERNAL_SECRET=local-workbench-secret \ OLONS_SERVER_MYSQL_DSN='user:password@tcp(host:3306)/olons_admin?parseTime=true&charset=utf8mb4&loc=Local'
go run ./cmd/workbench OLONS_SERVER_REDIS_ADDR='127.0.0.1:6379'
OLONS_SERVER_FINANCE_MASTER_KEY='<至少 32 个字符的固定资金安全密钥>'
OLONS_SERVER_API_KEYS='<API Key,多个以逗号分隔>'
``` ```
默认监听 `:6061`,默认连接本地 `workbench` 库和本地 Redis。 `finance_master_key` 已在 JSON 配置中提供固定值,也可由 `OLONS_SERVER_FINANCE_MASTER_KEY` 覆盖。服务会从这个主密钥派生数据加密键和检索键,用于保护收款账户姓名、账号、银行卡预留手机号及取出快照等支付敏感信息。密钥必须与已有数据保持一致;会员登录手机号直接保存在 `hg_app_member.mobile`。
开发联调可在 `configs/workbench.local.json` 打开 `allow_dev_header_auth`,请求头带: 前端静态目录可通过 `OLONS_SERVER_FRONTEND_DIR` 覆盖,默认值为 `web/dist`。本地开发可分别启动前后端:
```text
X-User-ID: 1
X-Tenant-ID: 1
X-Tenant-Code: default
```
正式链路使用:
```text
Authorization: Bearer {token}
X-MeetingIM-APIKey: {apikey}
X-Tenant-Code: default
X-IM-Schema: 1
X-Request-ID: {uuid}
```
服务从 Redis 读取 `im:session:token:{sha256(token)}` 校验登录态。
`usr...` 用户 ID 编解码优先从 IM Manager 内部配置接口读取 UID 加密 key,读不到时回退到
`auth.uid_key` 或环境变量 `IM_WORKBENCH_UID_KEY`。测试环境需要配置:
```text
IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com
IM_WORKBENCH_MANAGER_SERVICE_NAME=im-workbench
IM_WORKBENCH_MANAGER_SECRET={IM_CONFIG_API_SECRET}
```
默认读取 `GET /internal/im-config/third-party/im?keys=uid_encryption_key,UID_ENCRYPTION_KEY,uid_key`。
如果后台配置使用了其他 group/key,可通过 `IM_WORKBENCH_MANAGER_UID_KEY_GROUP` 和
`IM_WORKBENCH_MANAGER_UID_KEY_KEYS` 调整。
HotGo 后台访问 `/internal/admin/workbench/*` 时需要配置同一组内部鉴权参数:
```text
IM_WORKBENCH_INTERNAL_SERVICE_NAME=hotgo-admin
IM_WORKBENCH_INTERNAL_SECRET={shared-secret}
```
测试环境也可以复用已有内部服务密钥:HotGo 的 `IM_WORKBENCH_API_SECRET` 为空时会使用
`IM_INTERNAL_API_SECRET`,`im_workbench` 的 `IM_WORKBENCH_INTERNAL_SECRET` 为空时也会使用
`IM_INTERNAL_API_SECRET`。
HotGo 后台在测试集群中建议直连工作台服务内网地址:
```text
IM_WORKBENCH_API_BASE_URL=http://im-workbench.workbench-system.svc.cluster.local:6061
```
测试镜像默认使用 `hotgo-workbench-internal` 作为工作台内部签名密钥;正式环境建议通过
`IM_WORKBENCH_API_SECRET` 和 `IM_WORKBENCH_INTERNAL_SECRET` 覆盖为真实密钥。
## Kafka 事件
工作台写操作统一投递到 `im.workbench.event`。本地配置默认关闭 Kafka:
```bash ```bash
IM_WORKBENCH_KAFKA_ENABLED=true \ make web-install
IM_WORKBENCH_KAFKA_BROKERS=127.0.0.1:9092 \ make web-build
IM_WORKBENCH_CONFIG=configs/workbench.local.json \ make run
go run ./cmd/workbench
``` ```
测试环境使用 `configs/workbench.test.json`,Kafka 为明文 PLAINTEXT 直连,不配置 SASL/TLS。 生产镜像使用 Docker 多阶段构建自动安装前端依赖、构建 `web/`,并将 C 端产物部署到网站根路径 `/`。
## AI / DeepSeek 本地配置 ## 请求头
工作台 AI 首版接入 DeepSeek。默认配置已在 `configs/workbench.local.json` 中给出,实际调用前需要配置 API Key: ```http
X-API-Key: <api key>
```bash X-Request-ID: <request id>
DEEPSEEK_API_KEY=sk-xxx \ X-Device-ID: <optional device id>
IM_WORKBENCH_CONFIG=configs/workbench.local.json \
go run ./cmd/workbench
``` ```
可选参数: 服务采用单租户模式,登录仅按手机号查找账号,不读取或接受租户环境变量。现有数据库中的 `tenant_id` 字段仅作为兼容字段保留,新注册数据固定写入 `0`。
```bash ## 验证
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_DEFAULT_MODEL=deepseek-flash
DEEPSEEK_COMPLEX_MODEL=deepseek-v4-pro
DEEPSEEK_TIMEOUT_MS=60000
DEEPSEEK_MAX_RETRIES=2
DEEPSEEK_TEMPERATURE=0.2
DEEPSEEK_MAX_OUTPUT_TOKENS=4096
```
事件体字段:
```json
{
"event_id": "wbe_xxx",
"trace_id": "request-id",
"tenant_id": 1,
"tenant_code": "default",
"actor_user_id": 42,
"module": "meeting",
"action": "meeting.created",
"resource_id": "mtg_xxx",
"target_user_ids": [42, 43],
"title": "会议通知",
"summary": "项目周会",
"occurred_at": "2026-09-13T14:00:00Z",
"data": {}
}
```
## 构建
```bash ```bash
make test make test
make build make build
``` ```
## 数据库原则 前后端完整对接契约见 [`docs/移动端任务与账户平台前后端对接文档.md`](docs/移动端任务与账户平台前后端对接文档.md),已实现的注册登录简版契约见 [`docs/注册登录对接文档.md`](docs/注册登录对接文档.md)。
服务启动不自动建库建表,不执行 `CREATE TABLE` / `ALTER TABLE`。所有表结构和种子数据继续通过 SQL 审核后人工执行。
+268
View File
@@ -0,0 +1,268 @@
package main
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"log"
"os"
"regexp"
"strings"
"git.118im.com/im_team/olons_server/internal/config"
_ "github.com/go-sql-driver/mysql"
)
var phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`)
type memberPhone struct {
id int64
tenantID int64
ciphertext string
hash string
mask string
phone string
}
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
db, err := sql.Open("mysql", strings.TrimPrefix(cfg.MySQL.DSN, "mysql://"))
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
hasMobile, err := columnExists(ctx, db, "mobile")
if err != nil {
log.Fatal(err)
}
hasCipher, err := columnExists(ctx, db, "mobile_cipher")
if err != nil {
log.Fatal(err)
}
if hasMobile && !hasCipher {
log.Print("hg_app_member already uses the mobile column")
return
}
if !hasCipher {
log.Fatal("mobile_cipher is missing before migration")
}
aeads := newAEADs(append(splitKeys(os.Getenv("OLONS_SERVER_MEMBER_MIGRATION_DATA_KEYS")), cfg.Auth.DataEncryptionKey))
lookupKeys := decodeKeys(append(splitKeys(os.Getenv("OLONS_SERVER_MEMBER_MIGRATION_LOOKUP_KEYS")), cfg.Auth.LookupHMACKey))
overrides, err := parseOverrides(os.Getenv("OLONS_SERVER_MEMBER_MIGRATION_OVERRIDES"))
if err != nil {
log.Fatal(err)
}
if len(aeads) == 0 && len(lookupKeys) == 0 {
log.Fatal("no valid historical phone keys were provided")
}
phones, err := loadPhones(ctx, db, aeads, lookupKeys, overrides)
if err != nil {
log.Fatal(err)
}
if err := validateUnique(phones); err != nil {
log.Fatal(err)
}
if !hasMobile {
if _, err := db.ExecContext(ctx, `ALTER TABLE hg_app_member ADD COLUMN mobile varchar(20) NULL AFTER username`); err != nil {
log.Fatal(err)
}
}
if err := updatePhones(ctx, db, phones); err != nil {
log.Fatal(err)
}
if _, err := db.ExecContext(ctx, `ALTER TABLE hg_app_member
MODIFY COLUMN mobile varchar(20) NOT NULL,
DROP INDEX uk_app_member_mobile_hash,
ADD UNIQUE KEY uk_app_member_mobile (tenant_id,mobile),
DROP COLUMN mobile_cipher,
DROP COLUMN mobile_hash,
DROP COLUMN mobile_mask`); err != nil {
log.Fatal(err)
}
log.Printf("migrated %d member phone numbers", len(phones))
}
func columnExists(ctx context.Context, db *sql.DB, column string) (bool, error) {
var count int
err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='hg_app_member' AND COLUMN_NAME=?`, column).Scan(&count)
return count == 1, err
}
func loadPhones(ctx context.Context, db *sql.DB, aeads []cipher.AEAD, lookupKeys [][]byte, overrides map[int64]string) ([]memberPhone, error) {
rows, err := db.QueryContext(ctx, `SELECT id,tenant_id,mobile_cipher,mobile_hash,mobile_mask FROM hg_app_member ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
phones := make([]memberPhone, 0)
failed := make([]string, 0)
for rows.Next() {
var item memberPhone
if err := rows.Scan(&item.id, &item.tenantID, &item.ciphertext, &item.hash, &item.mask); err != nil {
return nil, err
}
item.phone = recoverPhone(item, aeads, lookupKeys)
if item.phone == "" {
item.phone = overrides[item.id]
}
if !phonePattern.MatchString(item.phone) {
failed = append(failed, fmt.Sprintf("%d", item.id))
} else if len(item.mask) == 11 && item.phone[:3]+"****"+item.phone[7:] != item.mask {
return nil, fmt.Errorf("member %d override does not match its phone mask", item.id)
}
phones = append(phones, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(failed) > 0 {
return nil, fmt.Errorf("phone recovery failed for member IDs: %s", strings.Join(failed, ","))
}
return phones, nil
}
func validateUnique(phones []memberPhone) error {
seen := make(map[string]int64, len(phones))
for _, item := range phones {
key := fmt.Sprintf("%d:%s", item.tenantID, item.phone)
if otherID, ok := seen[key]; ok {
return fmt.Errorf("members %d and %d have duplicate phone numbers", otherID, item.id)
}
seen[key] = item.id
}
return nil
}
func updatePhones(ctx context.Context, db *sql.DB, phones []memberPhone) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, item := range phones {
result, err := tx.ExecContext(ctx, `UPDATE hg_app_member SET mobile=? WHERE id=? AND tenant_id=?`, item.phone, item.id, item.tenantID)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return fmt.Errorf("member %d update affected %d rows", item.id, affected)
}
}
return tx.Commit()
}
func splitKeys(value string) []string {
return strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == '\n' })
}
func parseOverrides(value string) (map[int64]string, error) {
result := make(map[int64]string)
for _, pair := range splitKeys(value) {
var id int64
var phone string
if _, err := fmt.Sscanf(pair, "%d=%s", &id, &phone); err != nil || id <= 0 || !phonePattern.MatchString(phone) {
return nil, fmt.Errorf("invalid member phone override")
}
result[id] = phone
}
return result, nil
}
func newAEADs(encodedKeys []string) []cipher.AEAD {
result := make([]cipher.AEAD, 0, len(encodedKeys))
for _, encodedKey := range encodedKeys {
key, err := decodeKey(encodedKey)
if err != nil || len(key) != 32 {
continue
}
block, err := aes.NewCipher(key)
if err != nil {
continue
}
aead, err := cipher.NewGCM(block)
if err == nil {
result = append(result, aead)
}
}
return result
}
func decodeKeys(encodedKeys []string) [][]byte {
result := make([][]byte, 0, len(encodedKeys))
for _, encodedKey := range encodedKeys {
key, err := decodeKey(encodedKey)
if err == nil && len(key) >= 32 {
result = append(result, key)
}
}
return result
}
func decodeKey(value string) ([]byte, error) {
value = strings.TrimSpace(value)
if decoded, err := base64.StdEncoding.DecodeString(value); err == nil {
return decoded, nil
}
return base64.RawStdEncoding.DecodeString(value)
}
func recoverPhone(item memberPhone, aeads []cipher.AEAD, lookupKeys [][]byte) string {
for _, aead := range aeads {
for _, encoding := range []*base64.Encoding{base64.RawStdEncoding, base64.RawURLEncoding} {
plain, err := decrypt(aead, encoding, item.ciphertext)
phone := strings.TrimPrefix(plain, "+86")
if err == nil && phonePattern.MatchString(phone) {
return phone
}
}
}
if len(item.mask) != 11 || item.mask[3:7] != "****" {
return ""
}
for middle := 0; middle < 10000; middle++ {
phone := fmt.Sprintf("%s%04d%s", item.mask[:3], middle, item.mask[7:])
for _, value := range []string{phone, "+86" + phone} {
digest := sha256.Sum256([]byte(value))
if hmac.Equal([]byte(strings.ToLower(item.hash)), []byte(hex.EncodeToString(digest[:]))) {
return phone
}
}
for _, key := range lookupKeys {
for _, value := range []string{phone, "+86" + phone} {
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(value))
if hmac.Equal([]byte(strings.ToLower(item.hash)), []byte(hex.EncodeToString(mac.Sum(nil)))) {
return phone
}
}
}
}
return ""
}
func decrypt(aead cipher.AEAD, encoding *base64.Encoding, value string) (string, error) {
raw, err := encoding.DecodeString(value)
if err != nil || len(raw) < aead.NonceSize() {
return "", errors.New("invalid ciphertext")
}
plain, err := aead.Open(nil, raw[:aead.NonceSize()], raw[aead.NonceSize():], nil)
return string(plain), err
}
@@ -10,8 +10,8 @@ import (
"syscall" "syscall"
"time" "time"
"git.118im.com/im_team/im_workbench/internal/app" "git.118im.com/im_team/olons_server/internal/app"
"git.118im.com/im_team/im_workbench/internal/config" "git.118im.com/im_team/olons_server/internal/config"
) )
func main() { func main() {
@@ -34,7 +34,7 @@ func main() {
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
log.Printf("im_workbench listening on %s", cfg.Listen) log.Printf("olons_server listening on %s", cfg.Listen)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err errCh <- err
} }
+25
View File
@@ -0,0 +1,25 @@
{
"listen": ":6061",
"frontend_dir": "web/dist",
"mysql": {
"dsn": "root:root@tcp(192.168.1.135:3306)/olons_admin?parseTime=true&charset=utf8mb4&loc=Local",
"max_open_conns": 15,
"max_idle_conns": 5
},
"redis": {
"addr": "127.0.0.1:6379",
"db": 0
},
"upload": {
"dir": "storage/uploads",
"public_path": "/uploads"
},
"auth": {
"api_keys": [],
"session_key_pattern": "olons_server:session:token:%s",
"session_ttl_seconds": 1209600,
"finance_master_key": "olons-finance-master-key-v1-20261013",
"allow_dev_header_auth": true,
"required_api_key": false
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"listen": ":6061",
"frontend_dir": "web/dist",
"mysql": {
"dsn": "",
"max_open_conns": 15,
"max_idle_conns": 5
},
"redis": {
"addr": "",
"db": 0
},
"upload": {
"dir": "storage/uploads",
"public_path": "/uploads"
},
"auth": {
"api_keys": [],
"session_key_pattern": "olons_server:session:token:%s",
"session_ttl_seconds": 1209600,
"finance_master_key": "olons-finance-master-key-v1-20261013",
"required_api_key": true
}
}
-57
View File
@@ -1,57 +0,0 @@
{
"listen": ":6061",
"mysql": {
"dsn": "root:root@tcp(127.0.0.1:3306)/workbench?parseTime=true&charset=utf8mb4,utf8&loc=Local",
"max_open_conns": 15,
"max_idle_conns": 5
},
"redis": {
"addr": "127.0.0.1:6379",
"db": 0,
"key_prefix": "im:"
},
"kafka": {
"enabled": true,
"brokers": [
"127.0.0.1:9092"
],
"topic": "im.workbench.event",
"client_id": "im_workbench_local"
},
"auth": {
"api_keys": [],
"session_key_pattern": "im:session:token:%s",
"uid_key": "dkXxWLwqUNRfYmdXRzOkZw==",
"allow_dev_header_auth": true,
"required_tenant_code": true,
"required_api_key": false,
"skip_session_in_dev": false,
"internal_service_name": "hotgo-admin",
"internal_secret": "local-workbench-secret"
},
"manager": {
"base_url": "",
"service_name": "im-workbench",
"secret": "",
"uid_key_group": "im",
"uid_key_keys": [
"uid_encryption_key",
"UID_ENCRYPTION_KEY",
"uid_key"
],
"timeout_ms": 3000
},
"ai": {
"provider": "deepseek",
"default_model": "deepseek-flash",
"complex_model": "deepseek-v4-pro",
"temperature": 0.2,
"max_output_tokens": 4096,
"deepseek": {
"base_url": "https://api.deepseek.com",
"api_key": "",
"timeout_ms": 60000,
"max_retries": 2
}
}
}
-46
View File
@@ -1,46 +0,0 @@
{
"listen": ":6061",
"mysql": {
"dsn": "root:oWxz*2=sb),ouW%p@tcp(mysql.118im.com:30306)/meeting_im?parseTime=true&charset=utf8mb4,utf8&loc=Local"
},
"redis": {
"addr": "redis.118im.com:30637",
"password": "HMTJzkm.h>Y+E9ep",
"db": 0,
"key_prefix": "im:"
},
"kafka": {
"enabled": true,
"brokers": [
"kafka.118im.com:30835"
],
"topic": "im.workbench.event",
"username": "",
"password": "",
"sasl": "",
"client_id": "im_workbench_test"
},
"auth": {
"api_keys": [],
"session_key_pattern": "im:session:token:%s",
"uid_key": "dkXxWLwqUNRfYmdXRzOkZw==",
"allow_dev_header_auth": false,
"required_tenant_code": true,
"required_api_key": true,
"skip_session_in_dev": false,
"internal_service_name": "hotgo-admin",
"internal_secret": ""
},
"manager": {
"base_url": "https://admin.118im.com",
"service_name": "im-workbench",
"secret": "",
"uid_key_group": "im",
"uid_key_keys": [
"uid_encryption_key",
"UID_ENCRYPTION_KEY",
"uid_key"
],
"timeout_ms": 3000
}
}
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Some files were not shown because too many files have changed in this diff Show More