feat: 短信调试日志、Tawk 客服、手机号校验放宽与 Docker 文档

API
- 短信发码/验码/创蓝全链路结构化日志(手机号脱敏)
- 新增 SMS_DEBUG_LOG_CODE,联调时可输出验证码与 sessionId(对应创蓝批次号)
- 注册成功、短信找回密码成功写入审计相关日志
- 放宽手机号归一化:移除区号白名单与 10~15 位长度限制

Player
- 公告走马灯滚动周期调整为 35 秒
- 在线客服接入 Tawk.to(tawk.html),登录用户透传昵称/头像/ID
- 三语补充 support.connecting 文案

部署与文档
- docker-compose 与 .env.docker.example 增加 SMS_DEBUG_LOG_CODE
- 新增 docs/短信调试与日志说明.md、docs/docker 镜像构建导出脚本与说明
- Docker 部署指南补充镜像构建文档链接
- .gitignore 忽略 thebet365-images.tar 与 docker-build.log

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 14:08:00 +08:00
parent ff89c31b51
commit cb9a1e8708
21 changed files with 870 additions and 34 deletions

View File

@@ -81,7 +81,7 @@ const text = computed(() => {
.marquee-track {
display: flex;
width: max-content;
animation: marquee-scroll 22s linear infinite;
animation: marquee-scroll 35s linear infinite;
}
.marquee-text {

View File

@@ -1,19 +1,39 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { CUSTOMER_SERVICE_URL } from '../config/customerService';
import { buildCustomerServiceUrl } from '../config/customerService';
import { useAuthStore } from '../stores/auth';
import { usePlayerProfile } from '../composables/usePlayerProfile';
const props = defineProps<{ modelValue: boolean }>();
const emit = defineEmits<{ 'update:modelValue': [boolean] }>();
const { t } = useI18n();
const auth = useAuthStore();
const { profileRaw, avatarUrl } = usePlayerProfile();
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
const hasUrl = computed(() => Boolean(CUSTOMER_SERVICE_URL));
const iframeSrc = computed(() => {
const visitor = auth.user
? {
name:
profileRaw.value?.username ||
profileRaw.value?.preferences?.phone ||
auth.user.username ||
'',
avatar: avatarUrl.value
? new URL(avatarUrl.value, window.location.origin).href
: '',
id: String(profileRaw.value?.id ?? auth.user.id ?? ''),
}
: null;
return buildCustomerServiceUrl(t('support.connecting'), visitor);
});
function close() {
visible.value = false;
@@ -34,13 +54,13 @@ function close() {
<div class="cs-body">
<iframe
v-if="hasUrl"
v-if="visible"
:key="iframeSrc"
class="cs-frame"
:src="CUSTOMER_SERVICE_URL"
:src="iframeSrc"
:title="t('support.title')"
allow="clipboard-read; clipboard-write"
allow="microphone; camera; clipboard-read; clipboard-write"
/>
<p v-else class="cs-empty">{{ t('support.url_pending') }}</p>
</div>
</div>
</div>

View File

@@ -1,3 +1,25 @@
/** 客服 iframe 址,可通过环境变量 VITE_CUSTOMER_SERVICE_URL 覆盖 */
export const CUSTOMER_SERVICE_URL =
(import.meta.env.VITE_CUSTOMER_SERVICE_URL as string | undefined)?.trim() || '';
/** 客服 iframe 址,可通过 VITE_CUSTOMER_SERVICE_URL 覆盖(如外部客服页) */
export const CUSTOMER_SERVICE_BASE =
(import.meta.env.VITE_CUSTOMER_SERVICE_URL as string | undefined)?.trim() || '/tawk.html';
export type TawkVisitor = {
name?: string;
avatar?: string;
id?: string;
};
export function buildCustomerServiceUrl(
loadingText: string,
visitor?: TawkVisitor | null,
): string {
const params = new URLSearchParams({ loadingText });
if (visitor) {
if (visitor.name) params.set('name', visitor.name);
if (visitor.avatar) params.set('avatar', visitor.avatar);
if (visitor.id) params.set('id', visitor.id);
}
const separator = CUSTOMER_SERVICE_BASE.includes('?') ? '&' : '?';
return `${CUSTOMER_SERVICE_BASE}${separator}${params.toString()}`;
}

View File

@@ -122,6 +122,7 @@ export default {
title: 'Customer Support',
open: 'Open customer support',
close: 'Close',
connecting: 'Connecting to support...',
url_pending: 'Support URL is not configured yet.',
},
wallet: {

View File

@@ -128,6 +128,7 @@ export default {
title: 'Khidmat Pelanggan',
open: 'Buka khidmat pelanggan',
close: 'Tutup',
connecting: 'Menyambung ke khidmat pelanggan...',
url_pending: 'Pautan khidmat pelanggan belum dikonfigurasi.',
},
wallet: {

View File

@@ -122,6 +122,7 @@ export default {
title: '在线客服',
open: '打开在线客服',
close: '关闭',
connecting: '正在连接客服...',
url_pending: '客服链接暂未配置,请联系管理员。',
},
wallet: {