Compare commits
2 Commits
dfc352a64a
...
3bfcecd5bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfcecd5bb | |||
| ecae8b52c3 |
|
|
@ -90,3 +90,40 @@ export function getButlerQrcode() {
|
|||
export function setButlerQrcode(imageUrl: string) {
|
||||
return request.post('/admin/config/butlerQrcode', { imageUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员图标
|
||||
*/
|
||||
export function getMemberIcon() {
|
||||
return request.get('/admin/config/memberIcon')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会员图标
|
||||
*/
|
||||
export function setMemberIcon(imageUrl: string) {
|
||||
return request.post('/admin/config/memberIcon', { imageUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员图标配置
|
||||
*/
|
||||
export interface MemberIconsConfig {
|
||||
unlimitedMemberIcon?: string
|
||||
sincereMemberIcon?: string
|
||||
familyMemberIcon?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有会员图标
|
||||
*/
|
||||
export function getMemberIcons() {
|
||||
return request.get('/admin/config/memberIcons')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所有会员图标
|
||||
*/
|
||||
export function setMemberIcons(icons: MemberIconsConfig) {
|
||||
return request.post('/admin/config/memberIcons', icons)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,37 @@ const linkTypeOptions = [
|
|||
{ label: '小程序', value: 3 }
|
||||
]
|
||||
|
||||
// 内部页面选项
|
||||
const internalPageOptions = [
|
||||
{ label: '首页', value: '/pages/index/index' },
|
||||
{ label: '搜索页', value: '/pages/search/index' },
|
||||
{ label: '消息列表', value: '/pages/message/index' },
|
||||
{ label: '我的页面', value: '/pages/mine/index' },
|
||||
{ label: '会员中心', value: '/pages/member/index' },
|
||||
{ label: '填写资料', value: '/pages/profile/edit' },
|
||||
{ label: '实名认证', value: '/pages/realname/index' },
|
||||
{ label: '我的收藏', value: '/pages/favorite/index' },
|
||||
{ label: '浏览记录', value: '/pages/history/index' },
|
||||
{ label: '设置页面', value: '/pages/settings/index' },
|
||||
{ label: '关于我们', value: '/pages/about/index' },
|
||||
{ label: '用户协议', value: '/pages/agreement/user' },
|
||||
{ label: '隐私政策', value: '/pages/agreement/privacy' }
|
||||
]
|
||||
|
||||
// 链接类型提示
|
||||
const getLinkTypeTip = (linkType: number) => {
|
||||
switch (linkType) {
|
||||
case 1:
|
||||
return '选择小程序内部页面'
|
||||
case 2:
|
||||
return '请输入完整的网页地址,如:https://example.com'
|
||||
case 3:
|
||||
return '请输入小程序AppID和路径,格式:appId:path'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加Banner')
|
||||
|
|
@ -529,6 +560,7 @@ onMounted(() => {
|
|||
v-model="formData.linkType"
|
||||
placeholder="请选择链接类型"
|
||||
style="width: 100%"
|
||||
@change="formData.linkUrl = ''"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in linkTypeOptions"
|
||||
|
|
@ -539,10 +571,28 @@ onMounted(() => {
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址">
|
||||
<el-input
|
||||
<el-select
|
||||
v-if="formData.linkType === 1"
|
||||
v-model="formData.linkUrl"
|
||||
placeholder="请输入链接地址"
|
||||
placeholder="请选择内部页面"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in internalPageOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="formData.linkUrl"
|
||||
:placeholder="getLinkTypeTip(formData.linkType)"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
{{ getLinkTypeTip(formData.linkType) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="排序"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,37 @@ const linkTypeOptions = [
|
|||
{ label: '小程序', value: 3 }
|
||||
]
|
||||
|
||||
// 内部页面选项
|
||||
const internalPageOptions = [
|
||||
{ label: '首页', value: '/pages/index/index' },
|
||||
{ label: '搜索页', value: '/pages/search/index' },
|
||||
{ label: '消息列表', value: '/pages/message/index' },
|
||||
{ label: '我的页面', value: '/pages/mine/index' },
|
||||
{ label: '会员中心', value: '/pages/member/index' },
|
||||
{ label: '填写资料', value: '/pages/profile/edit' },
|
||||
{ label: '实名认证', value: '/pages/realname/index' },
|
||||
{ label: '我的收藏', value: '/pages/favorite/index' },
|
||||
{ label: '浏览记录', value: '/pages/history/index' },
|
||||
{ label: '设置页面', value: '/pages/settings/index' },
|
||||
{ label: '关于我们', value: '/pages/about/index' },
|
||||
{ label: '用户协议', value: '/pages/agreement/user' },
|
||||
{ label: '隐私政策', value: '/pages/agreement/privacy' }
|
||||
]
|
||||
|
||||
// 链接类型提示
|
||||
const getLinkTypeTip = (linkType: number) => {
|
||||
switch (linkType) {
|
||||
case 1:
|
||||
return '选择小程序内部页面'
|
||||
case 2:
|
||||
return '请输入完整的网页地址,如:https://example.com'
|
||||
case 3:
|
||||
return '请输入小程序AppID和路径,格式:appId:path'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加金刚位')
|
||||
|
|
@ -488,6 +519,7 @@ onMounted(() => {
|
|||
v-model="formData.linkType"
|
||||
placeholder="请选择链接类型"
|
||||
style="width: 100%"
|
||||
@change="formData.linkUrl = ''"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in linkTypeOptions"
|
||||
|
|
@ -498,10 +530,28 @@ onMounted(() => {
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址">
|
||||
<el-input
|
||||
<el-select
|
||||
v-if="formData.linkType === 1"
|
||||
v-model="formData.linkUrl"
|
||||
placeholder="请输入链接地址"
|
||||
placeholder="请选择内部页面"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in internalPageOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="formData.linkUrl"
|
||||
:placeholder="getLinkTypeTip(formData.linkType)"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
{{ getLinkTypeTip(formData.linkType) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="排序"
|
||||
|
|
|
|||
|
|
@ -116,12 +116,12 @@
|
|||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="现价(元)" prop="price">
|
||||
<el-input-number v-model="formData.price" :min="0" :precision="0" :controls="false" style="width: 100%" placeholder="1299" />
|
||||
<el-input-number v-model="formData.price" :min="0" :precision="2" :step="0.01" :controls="false" style="width: 100%" placeholder="1299" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="原价(元)" prop="originalPrice">
|
||||
<el-input-number v-model="formData.originalPrice" :min="0" :precision="0" :controls="false" style="width: 100%" placeholder="1899" />
|
||||
<el-input-number v-model="formData.originalPrice" :min="0" :precision="2" :step="0.01" :controls="false" style="width: 100%" placeholder="1899" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,73 @@
|
|||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 会员图标设置 -->
|
||||
<el-form-item label="不限时会员图标">
|
||||
<div class="icon-upload">
|
||||
<el-upload
|
||||
class="icon-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUnlimitedMemberIconSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="configForm.unlimitedMemberIcon" :src="getFullUrl(configForm.unlimitedMemberIcon)" class="icon-preview" />
|
||||
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="avatar-tip">
|
||||
<p>建议尺寸:64x64像素</p>
|
||||
<p>支持格式:PNG(透明背景)</p>
|
||||
<p>不限时会员用户显示的图标</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="诚意会员图标">
|
||||
<div class="icon-upload">
|
||||
<el-upload
|
||||
class="icon-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleSincereMemberIconSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="configForm.sincereMemberIcon" :src="getFullUrl(configForm.sincereMemberIcon)" class="icon-preview" />
|
||||
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="avatar-tip">
|
||||
<p>建议尺寸:64x64像素</p>
|
||||
<p>支持格式:PNG(透明背景)</p>
|
||||
<p>诚意会员用户显示的图标</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="家庭版会员图标">
|
||||
<div class="icon-upload">
|
||||
<el-upload
|
||||
class="icon-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleFamilyMemberIconSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="configForm.familyMemberIcon" :src="getFullUrl(configForm.familyMemberIcon)" class="icon-preview" />
|
||||
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="avatar-tip">
|
||||
<p>建议尺寸:64x64像素</p>
|
||||
<p>支持格式:PNG(透明背景)</p>
|
||||
<p>家庭版会员用户显示的图标</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveBasicConfig" :loading="saving">
|
||||
保存配置
|
||||
|
|
@ -144,7 +211,9 @@ import {
|
|||
getSearchBanner,
|
||||
setSearchBanner,
|
||||
getButlerQrcode,
|
||||
setButlerQrcode
|
||||
setButlerQrcode,
|
||||
getMemberIcons,
|
||||
setMemberIcons
|
||||
} from '@/api/config'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
|
|
@ -157,7 +226,10 @@ const activeTab = ref('basic')
|
|||
const configForm = ref({
|
||||
defaultAvatar: '',
|
||||
searchBanner: '',
|
||||
butlerQrcode: ''
|
||||
butlerQrcode: '',
|
||||
unlimitedMemberIcon: '',
|
||||
sincereMemberIcon: '',
|
||||
familyMemberIcon: ''
|
||||
})
|
||||
|
||||
const agreementForm = ref({
|
||||
|
|
@ -183,10 +255,11 @@ const getFullUrl = (url) => {
|
|||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const [avatarRes, bannerRes, qrcodeRes] = await Promise.all([
|
||||
const [avatarRes, bannerRes, qrcodeRes, memberIconsRes] = await Promise.all([
|
||||
getDefaultAvatar(),
|
||||
getSearchBanner(),
|
||||
getButlerQrcode()
|
||||
getButlerQrcode(),
|
||||
getMemberIcons()
|
||||
])
|
||||
if (avatarRes) {
|
||||
configForm.value.defaultAvatar = avatarRes.avatarUrl || ''
|
||||
|
|
@ -197,6 +270,11 @@ const loadConfig = async () => {
|
|||
if (qrcodeRes) {
|
||||
configForm.value.butlerQrcode = qrcodeRes.imageUrl || ''
|
||||
}
|
||||
if (memberIconsRes) {
|
||||
configForm.value.unlimitedMemberIcon = memberIconsRes.unlimitedMemberIcon || ''
|
||||
configForm.value.sincereMemberIcon = memberIconsRes.sincereMemberIcon || ''
|
||||
configForm.value.familyMemberIcon = memberIconsRes.familyMemberIcon || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
|
|
@ -248,6 +326,33 @@ const handleQrcodeSuccess = (response) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleUnlimitedMemberIconSuccess = (response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
configForm.value.unlimitedMemberIcon = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSincereMemberIconSuccess = (response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
configForm.value.sincereMemberIcon = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFamilyMemberIconSuccess = (response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
configForm.value.familyMemberIcon = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const beforeAvatarUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
|
@ -276,6 +381,15 @@ const saveBasicConfig = async () => {
|
|||
if (configForm.value.butlerQrcode) {
|
||||
promises.push(setButlerQrcode(configForm.value.butlerQrcode))
|
||||
}
|
||||
// 保存会员图标
|
||||
const memberIcons = {
|
||||
unlimitedMemberIcon: configForm.value.unlimitedMemberIcon || undefined,
|
||||
sincereMemberIcon: configForm.value.sincereMemberIcon || undefined,
|
||||
familyMemberIcon: configForm.value.familyMemberIcon || undefined
|
||||
}
|
||||
if (memberIcons.unlimitedMemberIcon || memberIcons.sincereMemberIcon || memberIcons.familyMemberIcon) {
|
||||
promises.push(setMemberIcons(memberIcons))
|
||||
}
|
||||
if (promises.length > 0) {
|
||||
await Promise.all(promises)
|
||||
ElMessage.success('保存成功')
|
||||
|
|
@ -484,6 +598,47 @@ onMounted(() => {
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
.icon-upload {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.icon-uploader {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.icon-uploader :deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-uploader :deep(.el-upload:hover) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.icon-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.agreement-editor {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,17 @@ export async function getOrCreateSession(targetUserId) {
|
|||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param {number} sessionId - 会话ID
|
||||
* @returns {Promise<Object>} 删除结果
|
||||
*/
|
||||
export async function deleteSession(sessionId) {
|
||||
const response = await post('/chat/session/delete', { sessionId })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传语音文件
|
||||
*
|
||||
|
|
@ -177,5 +188,6 @@ export default {
|
|||
respondExchange,
|
||||
getUnreadCount,
|
||||
getOrCreateSession,
|
||||
deleteSession,
|
||||
uploadVoice
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
<view class="title-row">
|
||||
<text class="gender-year">{{ genderText }} · {{ birthYear }}年</text>
|
||||
<view class="title-tags">
|
||||
<text v-if="isMember" class="tag tag-member">会员</text>
|
||||
<image v-if="isMember && memberIconUrl" class="member-icon" :src="memberIconUrl" mode="aspectFit" />
|
||||
<text v-else-if="isMember" class="tag tag-member">会员</text>
|
||||
<text v-if="isRealName" class="tag tag-realname">已实名</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -70,7 +71,8 @@
|
|||
<view class="title-row">
|
||||
<text class="gender-year">{{ genderText }} · {{ birthYear }}年</text>
|
||||
<view class="title-tags">
|
||||
<text v-if="isMember" class="tag tag-member">会员</text>
|
||||
<image v-if="isMember && memberIconUrl" class="member-icon" :src="memberIconUrl" mode="aspectFit" />
|
||||
<text v-else-if="isMember" class="tag tag-member">会员</text>
|
||||
<text v-if="isRealName" class="tag tag-realname">已实名</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -119,6 +121,7 @@
|
|||
|
||||
<script>
|
||||
import { getFullImageUrl } from '@/utils/image.js'
|
||||
import { useConfigStore } from '@/store/config.js'
|
||||
|
||||
export default {
|
||||
name: 'UserCard',
|
||||
|
|
@ -187,6 +190,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
memberLevel: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
isRealName: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
|
@ -221,6 +228,12 @@ export default {
|
|||
if (this.monthlyIncome < 20000) return '1.5万~2万/月'
|
||||
if (this.monthlyIncome < 30000) return '2万~3万/月'
|
||||
return '3万以上/月'
|
||||
},
|
||||
memberIconUrl() {
|
||||
if (!this.isMember || !this.memberLevel) return ''
|
||||
const configStore = useConfigStore()
|
||||
const iconUrl = configStore.getMemberIcon(this.memberLevel)
|
||||
return iconUrl ? getFullImageUrl(iconUrl) : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -279,6 +292,11 @@ export default {
|
|||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.member-icon {
|
||||
width: 120rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
|
|
|
|||
|
|
@ -24,12 +24,7 @@
|
|||
|
||||
<!-- 会员广告条 - 固定在底部 -->
|
||||
<view class="member-ad-section" v-if="showMemberAd">
|
||||
<view class="member-ad-bar" :style="memberAdBgStyle">
|
||||
<view class="ad-content" @click="handleMemberAdClick">
|
||||
<text class="ad-icon">👑</text>
|
||||
<text class="ad-text">{{ memberAdConfig?.title || '开通会员,解锁更多优质用户' }}</text>
|
||||
</view>
|
||||
<text class="ad-btn" @click="handleMemberAdClick">{{ memberAdConfig?.buttonText || '购买' }}</text>
|
||||
<view class="member-ad-bar" :style="memberAdBgStyle" @click="handleMemberAdClick">
|
||||
<view class="ad-close" @click.stop="handleCloseMemberAd">
|
||||
<text>×</text>
|
||||
</view>
|
||||
|
|
@ -101,6 +96,7 @@
|
|||
:height="user.height" :weight="user.weight" :education="user.education"
|
||||
:educationName="user.educationName" :occupation="user.occupation"
|
||||
:monthlyIncome="user.monthlyIncome" :intro="user.intro" :isMember="user.isMember"
|
||||
:memberLevel="user.memberLevel"
|
||||
:isRealName="user.isRealName" :isPhotoPublic="user.isPhotoPublic" :firstPhoto="user.firstPhoto"
|
||||
:viewedToday="user.viewedToday" @click="handleUserClick" @contact="handleUserContact" />
|
||||
</view>
|
||||
|
|
@ -310,11 +306,29 @@
|
|||
const handleBannerClick = (banner) => {
|
||||
if (!banner.linkUrl) return
|
||||
|
||||
// tabbar页面列表
|
||||
const tabbarPages = ['/pages/index/index', '/pages/message/index', '/pages/mine/index']
|
||||
|
||||
if (banner.linkType === 1) {
|
||||
uni.navigateTo({
|
||||
url: banner.linkUrl
|
||||
})
|
||||
// 内部页面
|
||||
if (tabbarPages.includes(banner.linkUrl)) {
|
||||
uni.switchTab({
|
||||
url: banner.linkUrl
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: banner.linkUrl,
|
||||
fail: (err) => {
|
||||
console.error('页面跳转失败:', err)
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (banner.linkType === 2) {
|
||||
// 外部链接 - 复制到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: banner.linkUrl,
|
||||
success: () => {
|
||||
|
|
@ -324,6 +338,22 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
} else if (banner.linkType === 3) {
|
||||
// 跳转其他小程序
|
||||
const [appId, path] = banner.linkUrl.split(':')
|
||||
if (appId) {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: appId,
|
||||
path: path || '',
|
||||
fail: (err) => {
|
||||
console.error('跳转小程序失败:', err)
|
||||
uni.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,10 +773,12 @@
|
|||
z-index: 99;
|
||||
|
||||
.member-ad-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
justify-content: center;
|
||||
min-height: 110rpx;
|
||||
padding: 0 24rpx;
|
||||
|
||||
.ad-content {
|
||||
display: flex;
|
||||
|
|
@ -776,6 +808,10 @@
|
|||
}
|
||||
|
||||
.ad-close {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -74,27 +74,43 @@
|
|||
|
||||
<!-- 聊天会话列表 -->
|
||||
<view class="session-list" v-if="sessions.length > 0">
|
||||
<view class="session-item" v-for="session in sessions" :key="session.sessionId"
|
||||
@click="handleSessionClick(session)">
|
||||
<!-- 头像 -->
|
||||
<view class="session-avatar">
|
||||
<image class="avatar-img" :src="session.targetAvatar || defaultAvatar" mode="aspectFill" />
|
||||
<!-- 未读徽章 -->
|
||||
<view class="unread-badge" v-if="session.unreadCount > 0">
|
||||
<text>{{ session.unreadCount > 99 ? '99+' : session.unreadCount }}</text>
|
||||
<view
|
||||
class="session-item-wrapper"
|
||||
v-for="session in sessions"
|
||||
:key="session.sessionId"
|
||||
>
|
||||
<view
|
||||
class="session-item"
|
||||
:style="{ transform: `translateX(${session.offsetX || 0}px)` }"
|
||||
@touchstart="handleTouchStart($event, session)"
|
||||
@touchmove="handleTouchMove($event, session)"
|
||||
@touchend="handleTouchEnd($event, session)"
|
||||
@click="handleSessionClick(session)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="session-avatar">
|
||||
<image class="avatar-img" :src="session.targetAvatar || defaultAvatar" mode="aspectFill" />
|
||||
<!-- 未读徽章 -->
|
||||
<view class="unread-badge" v-if="session.unreadCount > 0">
|
||||
<text>{{ session.unreadCount > 99 ? '99+' : session.unreadCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<view class="session-info">
|
||||
<view class="session-header">
|
||||
<text
|
||||
class="session-nickname">{{ session.targetNickname }}{{ session.relationship ? `(${session.relationship})` : '' }}</text>
|
||||
<text class="session-time">{{ formatTime(session.lastMessageTime) }}</text>
|
||||
</view>
|
||||
<view class="session-content">
|
||||
<text class="last-message">{{ session.lastMessage || '暂无消息' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<view class="session-info">
|
||||
<view class="session-header">
|
||||
<text
|
||||
class="session-nickname">{{ session.targetNickname }}{{ session.relationship ? `(${session.relationship})` : '' }}</text>
|
||||
<text class="session-time">{{ formatTime(session.lastMessageTime) }}</text>
|
||||
</view>
|
||||
<view class="session-content">
|
||||
<text class="last-message">{{ session.lastMessage || '暂无消息' }}</text>
|
||||
</view>
|
||||
<!-- 删除按钮 -->
|
||||
<view class="delete-btn" @click.stop="handleDeleteSession(session)">
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -138,7 +154,8 @@
|
|||
useConfigStore
|
||||
} from '@/store/config.js'
|
||||
import {
|
||||
getSessions
|
||||
getSessions,
|
||||
deleteSession
|
||||
} from '@/api/chat.js'
|
||||
import {
|
||||
getViewedMe,
|
||||
|
|
@ -172,6 +189,10 @@
|
|||
unlockedMe: 0
|
||||
})
|
||||
|
||||
// 滑动删除相关
|
||||
const touchStartX = ref(0)
|
||||
const deleteWidth = 80 // 删除按钮宽度
|
||||
|
||||
// 从 configStore 获取默认头像
|
||||
const defaultAvatar = computed(() => configStore.defaultAvatar || '/static/logo.png')
|
||||
|
||||
|
|
@ -328,12 +349,81 @@
|
|||
|
||||
// 点击会话
|
||||
const handleSessionClick = (session) => {
|
||||
// 如果正在滑动状态,先复位
|
||||
if (session.offsetX && session.offsetX < 0) {
|
||||
session.offsetX = 0
|
||||
return
|
||||
}
|
||||
chatStore.setCurrentSession(session.sessionId)
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 滑动开始
|
||||
const handleTouchStart = (e, session) => {
|
||||
touchStartX.value = e.touches[0].clientX
|
||||
// 关闭其他已打开的滑动项
|
||||
sessions.value.forEach(s => {
|
||||
if (s.sessionId !== session.sessionId && s.offsetX) {
|
||||
s.offsetX = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 滑动中
|
||||
const handleTouchMove = (e, session) => {
|
||||
const currentX = e.touches[0].clientX
|
||||
const diff = currentX - touchStartX.value
|
||||
|
||||
// 只允许左滑
|
||||
if (diff < 0) {
|
||||
session.offsetX = Math.max(diff, -deleteWidth)
|
||||
} else if (session.offsetX < 0) {
|
||||
session.offsetX = Math.min(0, session.offsetX + diff)
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动结束
|
||||
const handleTouchEnd = (e, session) => {
|
||||
// 如果滑动超过一半,展开删除按钮,否则复位
|
||||
if (session.offsetX < -deleteWidth / 2) {
|
||||
session.offsetX = -deleteWidth
|
||||
} else {
|
||||
session.offsetX = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
const handleDeleteSession = async (session) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该聊天记录吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await deleteSession(session.sessionId)
|
||||
if (result?.success || result?.code === 0) {
|
||||
// 从列表中移除
|
||||
const index = sessions.value.findIndex(s => s.sessionId === session.sessionId)
|
||||
if (index > -1) {
|
||||
sessions.value.splice(index, 1)
|
||||
}
|
||||
// 同步到store
|
||||
chatStore.setSessions(sessions.value)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: result?.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error)
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转登录
|
||||
const handleLogin = () => {
|
||||
uni.navigateTo({
|
||||
|
|
@ -564,11 +654,37 @@
|
|||
|
||||
// 聊天会话列表
|
||||
.session-list {
|
||||
.session-item-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 160rpx;
|
||||
background-color: #ff4d4f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
background-color: #F3F3F3;
|
||||
transition: transform 0.2s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
|
|
|
|||
|
|
@ -143,10 +143,7 @@
|
|||
:src="photo.photoUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="!userDetail.isPhotoPublic && !isUnlocked" class="photo-lock-overlay">
|
||||
<text class="lock-icon">🔒</text>
|
||||
<text class="lock-text">私密</text>
|
||||
</view>
|
||||
<view v-if="!userDetail.isPhotoPublic && !isUnlocked" class="photo-lock-overlay"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -160,33 +157,41 @@
|
|||
</view>
|
||||
|
||||
<!-- 孩子的择偶标准 -->
|
||||
<view class="section-card" v-if="userDetail.requirement">
|
||||
<view class="section-card">
|
||||
<view class="section-title">孩子的择偶标准</view>
|
||||
<view class="requirement-grid">
|
||||
<view class="req-item" v-if="requirementAgeText">
|
||||
<view class="req-item">
|
||||
<text class="label">年龄范围</text>
|
||||
<text class="value">{{ requirementAgeText }}</text>
|
||||
</view>
|
||||
<view class="req-item" v-if="requirementHeightText">
|
||||
<view class="req-item">
|
||||
<text class="label">身高要求</text>
|
||||
<text class="value">{{ requirementHeightText }}</text>
|
||||
</view>
|
||||
<view class="req-item" v-if="requirementEducationText">
|
||||
<view class="req-item">
|
||||
<text class="label">学历要求</text>
|
||||
<text class="value">{{ requirementEducationText }}</text>
|
||||
</view>
|
||||
<view class="req-item" v-if="requirementCityText">
|
||||
<view class="req-item">
|
||||
<text class="label">城市要求</text>
|
||||
<text class="value">{{ requirementCityText }}</text>
|
||||
</view>
|
||||
<view class="req-item" v-if="requirementIncomeText">
|
||||
<text class="label">收入范围</text>
|
||||
<view class="req-item">
|
||||
<text class="label">收入要求</text>
|
||||
<text class="value">{{ requirementIncomeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="req-extra" v-if="requirementExtraText">
|
||||
<text class="label">其他</text>
|
||||
<text class="value">{{ requirementExtraText }}</text>
|
||||
<view class="req-item">
|
||||
<text class="label">房产要求</text>
|
||||
<text class="value">{{ requirementHouseText }}</text>
|
||||
</view>
|
||||
<view class="req-item">
|
||||
<text class="label">车产要求</text>
|
||||
<text class="value">{{ requirementCarText }}</text>
|
||||
</view>
|
||||
<view class="req-item">
|
||||
<text class="label">婚姻要求</text>
|
||||
<text class="value">{{ requirementMarriageText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -427,60 +432,65 @@ const zodiacText = computed(() => {
|
|||
return getZodiac(userDetail.value?.birthYear)
|
||||
})
|
||||
|
||||
// 择偶要求文本
|
||||
// 择偶要求文本 - 有范围的显示"不限",其他显示"无"
|
||||
const requirementAgeText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req) return ''
|
||||
if (!req) return '不限'
|
||||
if (req.ageMin && req.ageMax) return `${req.ageMin}-${req.ageMax}岁`
|
||||
if (req.ageMin) return `${req.ageMin}岁以上`
|
||||
if (req.ageMax) return `${req.ageMax}岁以下`
|
||||
return ''
|
||||
return '不限'
|
||||
})
|
||||
|
||||
const requirementHeightText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req) return ''
|
||||
if (!req) return '不限'
|
||||
if (req.heightMin && req.heightMax) return `${req.heightMin}cm-${req.heightMax}cm`
|
||||
if (req.heightMin) return `${req.heightMin}cm以上`
|
||||
if (req.heightMax) return `${req.heightMax}cm以下`
|
||||
return ''
|
||||
return '不限'
|
||||
})
|
||||
|
||||
const requirementEducationText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req || !req.education || req.education.length === 0) return ''
|
||||
if (!req || !req.education || req.education.length === 0) return '无'
|
||||
return req.education.map(e => educationMap[e]).filter(Boolean).join('、')
|
||||
})
|
||||
|
||||
const requirementCityText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req) return ''
|
||||
if (!req) return '无'
|
||||
const cities = []
|
||||
if (req.city1City) cities.push(req.city1City)
|
||||
if (req.city2City) cities.push(req.city2City)
|
||||
return cities.join('、') || ''
|
||||
return cities.length > 0 ? cities.join('、') : '无'
|
||||
})
|
||||
|
||||
const requirementIncomeText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req || !req.monthlyIncomeMin) return ''
|
||||
return incomeMap[req.monthlyIncomeMin] || ''
|
||||
if (!req || !req.monthlyIncomeMin) return '不限'
|
||||
return incomeMap[req.monthlyIncomeMin] || '不限'
|
||||
})
|
||||
|
||||
const requirementExtraText = computed(() => {
|
||||
// 房产要求文本
|
||||
const requirementHouseText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req) return ''
|
||||
const extras = []
|
||||
if (req.houseStatus && req.houseStatus.length > 0) {
|
||||
extras.push('房产要求: ' + req.houseStatus.map(h => houseMap[h]).filter(Boolean).join('、'))
|
||||
}
|
||||
if (req.carStatus && req.carStatus.length > 0) {
|
||||
extras.push('车辆要求: ' + req.carStatus.map(c => carMap[c]).filter(Boolean).join('、'))
|
||||
}
|
||||
if (req.marriageStatus && req.marriageStatus.length > 0) {
|
||||
extras.push('婚姻要求: ' + req.marriageStatus.map(m => marriageMap[m]).filter(Boolean).join('、'))
|
||||
}
|
||||
return extras.join(';')
|
||||
if (!req || !req.houseStatus || req.houseStatus.length === 0) return '无'
|
||||
return req.houseStatus.map(h => houseMap[h]).filter(Boolean).join('、')
|
||||
})
|
||||
|
||||
// 车产要求文本
|
||||
const requirementCarText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req || !req.carStatus || req.carStatus.length === 0) return '无'
|
||||
return req.carStatus.map(c => carMap[c]).filter(Boolean).join('、')
|
||||
})
|
||||
|
||||
// 婚姻要求文本
|
||||
const requirementMarriageText = computed(() => {
|
||||
const req = userDetail.value?.requirement
|
||||
if (!req || !req.marriageStatus || req.marriageStatus.length === 0) return '无'
|
||||
return req.marriageStatus.map(m => marriageMap[m]).filter(Boolean).join('、')
|
||||
})
|
||||
|
||||
// 加载用户详情
|
||||
|
|
@ -494,8 +504,6 @@ const loadUserDetail = async () => {
|
|||
isFavorited.value = res.data.isFavorited || false
|
||||
isUnlocked.value = res.data.isUnlocked || false
|
||||
remainingUnlockQuota.value = res.data.remainingUnlockQuota || 0
|
||||
|
||||
// 已登录用户记录浏览(后端已处理,这里不需要再调用)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户详情失败:', error)
|
||||
|
|
@ -944,8 +952,8 @@ onShareAppMessage(() => {
|
|||
height: 100%;
|
||||
|
||||
&.photo-blurred {
|
||||
filter: blur(20px);
|
||||
transform: scale(1.1);
|
||||
filter: blur(6px);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -955,22 +963,8 @@ onShareAppMessage(() => {
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
|
||||
.lock-icon {
|
||||
font-size: 40rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.lock-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.photo-blur {
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@
|
|||
|
||||
<!-- 期望结婚时间 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">期望结婚时间</text>
|
||||
<text class="form-label required">期望结婚时间</text>
|
||||
<picker
|
||||
:value="expectMarryIndex"
|
||||
:range="expectMarryOptions"
|
||||
|
|
@ -422,7 +422,7 @@
|
|||
<view class="card-title">意向城市</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">城市1</text>
|
||||
<text class="form-label required">城市1</text>
|
||||
<picker
|
||||
mode="region"
|
||||
:level="'city'"
|
||||
|
|
@ -520,8 +520,108 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤5: 联系方式 -->
|
||||
<!-- 步骤5: 父母情况 -->
|
||||
<view v-show="currentStep === 4" class="form-section">
|
||||
<view class="section-title">父母情况</view>
|
||||
|
||||
<!-- 是否独居 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">孩子是否独居</text>
|
||||
<view class="radio-group">
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: formData.isLivingAlone }"
|
||||
@click="formData.isLivingAlone = true"
|
||||
>
|
||||
<text>是</text>
|
||||
</view>
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: !formData.isLivingAlone }"
|
||||
@click="formData.isLivingAlone = false"
|
||||
>
|
||||
<text>否</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 父母情况 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label required">父母情况</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="parentStatusOptions"
|
||||
range-key="label"
|
||||
@change="handleParentStatusChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ formData.parentStatus || '请选择' }}
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 父母是否退休 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">父母是否退休</text>
|
||||
<view class="radio-group">
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: formData.isParentRetired }"
|
||||
@click="formData.isParentRetired = true"
|
||||
>
|
||||
<text>已退休</text>
|
||||
</view>
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: !formData.isParentRetired }"
|
||||
@click="formData.isParentRetired = false"
|
||||
>
|
||||
<text>未退休</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 父母现居城市 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">父母现居城市</text>
|
||||
<picker
|
||||
mode="region"
|
||||
:level="'city'"
|
||||
:value="[formData.parentProvince, formData.parentCity]"
|
||||
@change="handleParentCityChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ parentCityText || '请选择' }}
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 父母是否有房 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">父母是否有房</text>
|
||||
<view class="radio-group">
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: formData.parentHasHouse }"
|
||||
@click="formData.parentHasHouse = true"
|
||||
>
|
||||
<text>有</text>
|
||||
</view>
|
||||
<view
|
||||
class="radio-item"
|
||||
:class="{ active: !formData.parentHasHouse }"
|
||||
@click="formData.parentHasHouse = false"
|
||||
>
|
||||
<text>无</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤6: 联系方式 -->
|
||||
<view v-show="currentStep === 5" class="form-section">
|
||||
<view class="section-title">联系方式</view>
|
||||
|
||||
<!-- 微信号 -->
|
||||
|
|
@ -628,8 +728,9 @@ const handleBack = () => {
|
|||
const steps = [
|
||||
{ label: '基础信息' },
|
||||
{ label: '详细信息' },
|
||||
{ label: '介绍' },
|
||||
{ label: '要求' },
|
||||
{ label: '孩子介绍' },
|
||||
{ label: '择偶要求' },
|
||||
{ label: '父母情况' },
|
||||
{ label: '联系方式' }
|
||||
]
|
||||
|
||||
|
|
@ -664,6 +765,13 @@ const formData = reactive({
|
|||
introduction: '',
|
||||
weChatNo: '',
|
||||
phone: '', // 验证后的手机号
|
||||
// 父母情况
|
||||
isLivingAlone: false, // 是否独居
|
||||
parentStatus: '', // 父母情况
|
||||
isParentRetired: false, // 父母是否退休
|
||||
parentProvince: '', // 父母现居省
|
||||
parentCity: '', // 父母现居市
|
||||
parentHasHouse: false, // 父母是否有房
|
||||
requirement: {
|
||||
ageMin: 0,
|
||||
ageMax: 0,
|
||||
|
|
@ -733,6 +841,15 @@ const expectMarryOptions = [
|
|||
{ value: 3, label: '孩子满意就结婚' }
|
||||
]
|
||||
|
||||
// 父母情况选项
|
||||
const parentStatusOptions = [
|
||||
{ value: '父母健在', label: '父母健在' },
|
||||
{ value: '父亲去世', label: '父亲去世' },
|
||||
{ value: '母亲去世', label: '母亲去世' },
|
||||
{ value: '父母均已去世', label: '父母均已去世' },
|
||||
{ value: '父母离异', label: '父母离异' }
|
||||
]
|
||||
|
||||
// 出生年份选项 (Property 9: Birth Year Range)
|
||||
const birthYearOptions = computed(() => getBirthYearRange())
|
||||
|
||||
|
|
@ -896,6 +1013,14 @@ const city2Text = computed(() => {
|
|||
return ''
|
||||
})
|
||||
|
||||
// 父母现居城市文本
|
||||
const parentCityText = computed(() => {
|
||||
if (formData.parentProvince && formData.parentCity) {
|
||||
return `${formData.parentProvince} ${formData.parentCity}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 监听关系和姓氏变化,自动生成昵称 (Property 8: Nickname Auto-Generation)
|
||||
watch(
|
||||
() => [formData.relationship, formData.surname, formData.childGender],
|
||||
|
|
@ -1006,6 +1131,18 @@ const handleCity2Change = (e) => {
|
|||
formData.requirement.city2City = city
|
||||
}
|
||||
|
||||
// 父母情况选择
|
||||
const handleParentStatusChange = (e) => {
|
||||
formData.parentStatus = parentStatusOptions[e.detail.value].value
|
||||
}
|
||||
|
||||
// 父母现居城市选择
|
||||
const handleParentCityChange = (e) => {
|
||||
const [province, city] = e.detail.value
|
||||
formData.parentProvince = province
|
||||
formData.parentCity = city
|
||||
}
|
||||
|
||||
// 多选切换
|
||||
const toggleEducationRequirement = (value) => {
|
||||
const idx = formData.requirement.education.indexOf(value)
|
||||
|
|
@ -1208,6 +1345,10 @@ const validateStep = (step) => {
|
|||
uni.showToast({ title: '请选择婚姻状态', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!formData.expectMarryTime) {
|
||||
uni.showToast({ title: '请选择期望结婚时间', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
case 2: // 自我介绍
|
||||
|
|
@ -1218,10 +1359,21 @@ const validateStep = (step) => {
|
|||
return true
|
||||
|
||||
case 3: // 择偶要求
|
||||
// 择偶要求为可选,不做强制验证
|
||||
// 意向城市至少选择1个
|
||||
if (!formData.requirement.city1Province || !formData.requirement.city1City) {
|
||||
uni.showToast({ title: '请至少选择一个意向城市', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
case 4: // 联系方式
|
||||
case 4: // 父母情况
|
||||
if (!formData.parentStatus) {
|
||||
uni.showToast({ title: '请选择父母情况', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
case 5: // 联系方式
|
||||
if (!formData.weChatNo) {
|
||||
uni.showToast({ title: '请输入微信号', icon: 'none' })
|
||||
return false
|
||||
|
|
@ -1326,6 +1478,14 @@ const loadProfile = async () => {
|
|||
formData.introduction = profile.introduction || ''
|
||||
formData.weChatNo = profile.weChatNo || ''
|
||||
|
||||
// 父母情况
|
||||
formData.isLivingAlone = profile.isLivingAlone || false
|
||||
formData.parentStatus = profile.parentStatus || ''
|
||||
formData.isParentRetired = profile.isParentRetired || false
|
||||
formData.parentProvince = profile.parentProvince || ''
|
||||
formData.parentCity = profile.parentCity || ''
|
||||
formData.parentHasHouse = profile.parentHasHouse || false
|
||||
|
||||
// 手机号
|
||||
if (profile.phone) {
|
||||
formData.phone = profile.phone
|
||||
|
|
|
|||
|
|
@ -281,11 +281,11 @@
|
|||
try {
|
||||
const res = await get('/realname/status')
|
||||
if (res && res.success && res.data) {
|
||||
if (res.data.isVerified) {
|
||||
if (res.data.isRealName) {
|
||||
userStore.setRealNameStatus(true)
|
||||
verificationResult.value = {
|
||||
name: res.data.name,
|
||||
idNumber: res.data.idNumber
|
||||
name: res.data.name || res.data.maskedName,
|
||||
idNumber: res.data.idNumber || res.data.maskedIdCard
|
||||
}
|
||||
} else if (res.data.isPaid) {
|
||||
// 已支付但未完成上传
|
||||
|
|
@ -471,34 +471,38 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* 上传身份证照片
|
||||
* 上传身份证照片进行实名认证
|
||||
*/
|
||||
const uploadIdCards = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken()
|
||||
|
||||
// 先上传正面
|
||||
// 同时上传正反面
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/realname/verify`,
|
||||
url: `${BASE_URL}/api/app/realname/verify`,
|
||||
filePath: idCardFront.value,
|
||||
name: 'frontImage',
|
||||
formData: {
|
||||
backImagePath: idCardBack.value
|
||||
},
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
success: async (uploadRes) => {
|
||||
if (uploadRes.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data)
|
||||
|
||||
// 如果需要单独上传反面,再上传
|
||||
if (data.needBackImage) {
|
||||
const backResult = await uploadBackImage(token)
|
||||
resolve(backResult)
|
||||
const response = JSON.parse(uploadRes.data)
|
||||
|
||||
// 检查API响应
|
||||
if (response.success || response.code === 0) {
|
||||
const data = response.data
|
||||
|
||||
// 如果需要单独上传反面
|
||||
if (data.needBackImage) {
|
||||
const backResult = await uploadBackImage(token)
|
||||
resolve(backResult)
|
||||
} else {
|
||||
resolve(response)
|
||||
}
|
||||
} else {
|
||||
resolve(data)
|
||||
reject(new Error(response.message || '认证失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('解析响应失败'))
|
||||
|
|
@ -515,6 +519,7 @@
|
|||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('上传失败:', err)
|
||||
reject(new Error('网络连接失败'))
|
||||
}
|
||||
})
|
||||
|
|
@ -527,7 +532,7 @@
|
|||
const uploadBackImage = (token) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/realname/verify/back`,
|
||||
url: `${BASE_URL}/api/app/realname/verify/back`,
|
||||
filePath: idCardBack.value,
|
||||
name: 'backImage',
|
||||
header: {
|
||||
|
|
@ -536,13 +541,23 @@
|
|||
success: (uploadRes) => {
|
||||
if (uploadRes.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data)
|
||||
resolve(data)
|
||||
const response = JSON.parse(uploadRes.data)
|
||||
|
||||
if (response.success || response.code === 0) {
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(new Error(response.message || '上传失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error('上传失败'))
|
||||
try {
|
||||
const errorData = JSON.parse(uploadRes.data)
|
||||
reject(new Error(errorData.message || '上传失败'))
|
||||
} catch (e) {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
|
|
|
|||
|
|
@ -137,12 +137,12 @@
|
|||
</scroll-view>
|
||||
|
||||
<!-- 年龄选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showAgePopup" @click.self="showAgePopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showAgePopup" @click.self="showAgePopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showAgePopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showAgePopup = false">取消</text>
|
||||
<text class="picker-title">年龄要求</text>
|
||||
<text class="picker-confirm" @click="confirmAge">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmAge">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="agePickerValue" @change="onAgeChange" class="picker-view">
|
||||
|
|
@ -161,12 +161,12 @@
|
|||
</view>
|
||||
|
||||
<!-- 身高选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showHeightPopup" @click.self="showHeightPopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showHeightPopup" @click.self="showHeightPopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showHeightPopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showHeightPopup = false">取消</text>
|
||||
<text class="picker-title">身高要求</text>
|
||||
<text class="picker-confirm" @click="confirmHeight">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmHeight">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="heightPickerValue" @change="onHeightChange" class="picker-view">
|
||||
|
|
@ -185,12 +185,12 @@
|
|||
</view>
|
||||
|
||||
<!-- 收入选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showIncomePopup" @click.self="showIncomePopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showIncomePopup" @click.self="showIncomePopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showIncomePopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showIncomePopup = false">取消</text>
|
||||
<text class="picker-title">月收入要求</text>
|
||||
<text class="picker-confirm" @click="confirmIncome">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmIncome">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="incomePickerValue" @change="onIncomeChange" class="picker-view">
|
||||
|
|
@ -203,12 +203,12 @@
|
|||
</view>
|
||||
|
||||
<!-- 房产选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showHousePopup" @click.self="showHousePopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showHousePopup" @click.self="showHousePopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showHousePopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showHousePopup = false">取消</text>
|
||||
<text class="picker-title">房产要求</text>
|
||||
<text class="picker-confirm" @click="confirmHouse">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmHouse">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="housePickerValue" @change="onHouseChange" class="picker-view">
|
||||
|
|
@ -221,12 +221,12 @@
|
|||
</view>
|
||||
|
||||
<!-- 车产选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showCarPopup" @click.self="showCarPopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showCarPopup" @click.self="showCarPopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showCarPopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showCarPopup = false">取消</text>
|
||||
<text class="picker-title">车产要求</text>
|
||||
<text class="picker-confirm" @click="confirmCar">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmCar">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="carPickerValue" @change="onCarChange" class="picker-view">
|
||||
|
|
@ -239,12 +239,12 @@
|
|||
</view>
|
||||
|
||||
<!-- 婚姻选择弹窗 -->
|
||||
<view class="picker-popup" v-if="showMarriagePopup" @click.self="showMarriagePopup = false">
|
||||
<view class="picker-content">
|
||||
<view class="picker-popup" v-if="showMarriagePopup" @click.self="showMarriagePopup = false" @touchmove.prevent>
|
||||
<view class="picker-content" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-cancel" @click="showMarriagePopup = false">取消</text>
|
||||
<text class="picker-cancel" @click.stop="showMarriagePopup = false">取消</text>
|
||||
<text class="picker-title">婚姻要求</text>
|
||||
<text class="picker-confirm" @click="confirmMarriage">确定</text>
|
||||
<text class="picker-confirm" @click.stop="confirmMarriage">确定</text>
|
||||
</view>
|
||||
<view class="picker-body">
|
||||
<picker-view :value="marriagePickerValue" @change="onMarriageChange" class="picker-view">
|
||||
|
|
|
|||
|
|
@ -24,32 +24,70 @@
|
|||
|
||||
<!-- 搜索条件标签 -->
|
||||
<view class="search-tags">
|
||||
<view class="tag" v-if="searchParams.ageMin || searchParams.ageMax">
|
||||
年龄{{ searchParams.ageMin || 18 }}-{{ searchParams.ageMax || 60 }}岁
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.heightMin || searchParams.heightMax">
|
||||
身高{{ searchParams.heightMin || 150 }}cm及以上
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.cities && searchParams.cities.length > 0">
|
||||
地区{{ searchParams.cities[0] }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.carStatus">
|
||||
{{ searchParams.carStatus === 1 ? '有车' : '车产不限' }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.houseStatus">
|
||||
{{ searchParams.houseStatus === 1 ? '有房' : '房产不限' }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.marriageStatus">
|
||||
{{ getMarriageText(searchParams.marriageStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.monthlyIncome">
|
||||
{{ getIncomeText(searchParams.monthlyIncome) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.education && searchParams.education.length > 0">
|
||||
{{ getEducationText(searchParams.education[0]) }}
|
||||
</view>
|
||||
<template v-if="showAllTags">
|
||||
<!-- 展开状态:显示所有标签 -->
|
||||
<view class="tag" v-if="searchParams.ageMin || searchParams.ageMax">
|
||||
年龄{{ searchParams.ageMin || 18 }}-{{ searchParams.ageMax || 60 }}岁
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.heightMin || searchParams.heightMax">
|
||||
身高{{ searchParams.heightMin || 150 }}cm及以上
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.cities && searchParams.cities.length > 0">
|
||||
地区{{ searchParams.cities[0] }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.carStatus">
|
||||
{{ getCarText(searchParams.carStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.houseStatus">
|
||||
{{ getHouseText(searchParams.houseStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.marriageStatus">
|
||||
{{ getMarriageText(searchParams.marriageStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.monthlyIncome">
|
||||
{{ getIncomeText(searchParams.monthlyIncome) }}
|
||||
</view>
|
||||
<view class="tag" v-if="searchParams.education && searchParams.education.length > 0">
|
||||
{{ getEducationText(searchParams.education[0]) }}
|
||||
</view>
|
||||
<!-- 更多城市 -->
|
||||
<view class="tag" v-for="(city, index) in searchParams.cities?.slice(1)" :key="'city-' + index">
|
||||
地区{{ city }}
|
||||
</view>
|
||||
<!-- 更多学历 -->
|
||||
<view class="tag" v-for="(edu, index) in searchParams.education?.slice(1)" :key="'edu-' + index">
|
||||
{{ getEducationText(edu) }}
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 收起状态:只显示前6个标签 -->
|
||||
<view class="tag" v-if="visibleTags.includes('age') && (searchParams.ageMin || searchParams.ageMax)">
|
||||
年龄{{ searchParams.ageMin || 18 }}-{{ searchParams.ageMax || 60 }}岁
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('height') && (searchParams.heightMin || searchParams.heightMax)">
|
||||
身高{{ searchParams.heightMin || 150 }}cm及以上
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('city') && searchParams.cities && searchParams.cities.length > 0">
|
||||
地区{{ searchParams.cities[0] }}
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('car') && searchParams.carStatus">
|
||||
{{ getCarText(searchParams.carStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('house') && searchParams.houseStatus">
|
||||
{{ getHouseText(searchParams.houseStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('marriage') && searchParams.marriageStatus">
|
||||
{{ getMarriageText(searchParams.marriageStatus) }}
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('income') && searchParams.monthlyIncome">
|
||||
{{ getIncomeText(searchParams.monthlyIncome) }}
|
||||
</view>
|
||||
<view class="tag" v-if="visibleTags.includes('education') && searchParams.education && searchParams.education.length > 0">
|
||||
{{ getEducationText(searchParams.education[0]) }}
|
||||
</view>
|
||||
</template>
|
||||
<view class="tag more" v-if="hasMoreTags" @click="showAllTags = !showAllTags">
|
||||
{{ showAllTags ? '收起' : '更多...' }} ∨
|
||||
{{ showAllTags ? '收起 ∧' : '更多... ∨' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -183,17 +221,34 @@ const searchDate = ref('')
|
|||
|
||||
// 显示所有标签
|
||||
const showAllTags = ref(false)
|
||||
|
||||
// 计算所有标签
|
||||
const allTags = computed(() => {
|
||||
const tags = []
|
||||
const sp = searchParams.value
|
||||
if (sp.ageMin || sp.ageMax) tags.push('age')
|
||||
if (sp.heightMin || sp.heightMax) tags.push('height')
|
||||
if (sp.cities?.length > 0) tags.push('city')
|
||||
if (sp.carStatus) tags.push('car')
|
||||
if (sp.houseStatus) tags.push('house')
|
||||
if (sp.marriageStatus) tags.push('marriage')
|
||||
if (sp.monthlyIncome) tags.push('income')
|
||||
if (sp.education?.length > 0) tags.push('education')
|
||||
return tags
|
||||
})
|
||||
|
||||
// 可见标签(收起状态只显示前6个)
|
||||
const visibleTags = computed(() => {
|
||||
return allTags.value.slice(0, 6)
|
||||
})
|
||||
|
||||
const hasMoreTags = computed(() => {
|
||||
let count = 0
|
||||
if (searchParams.value.ageMin || searchParams.value.ageMax) count++
|
||||
if (searchParams.value.heightMin || searchParams.value.heightMax) count++
|
||||
if (searchParams.value.cities?.length > 0) count++
|
||||
if (searchParams.value.carStatus) count++
|
||||
if (searchParams.value.houseStatus) count++
|
||||
if (searchParams.value.marriageStatus) count++
|
||||
if (searchParams.value.monthlyIncome) count++
|
||||
if (searchParams.value.education?.length > 0) count++
|
||||
return count > 6
|
||||
const sp = searchParams.value
|
||||
let totalCount = allTags.value.length
|
||||
// 额外的城市和学历
|
||||
if (sp.cities?.length > 1) totalCount += sp.cities.length - 1
|
||||
if (sp.education?.length > 1) totalCount += sp.education.length - 1
|
||||
return totalCount > 6
|
||||
})
|
||||
|
||||
// 搜索状态
|
||||
|
|
@ -225,13 +280,30 @@ const incomeMap = {
|
|||
|
||||
const marriageMap = {
|
||||
1: '未婚',
|
||||
2: '离异',
|
||||
3: '丧偶'
|
||||
2: '离异未育',
|
||||
3: '离异已育'
|
||||
}
|
||||
|
||||
const houseMap = {
|
||||
1: '现居地已购房',
|
||||
2: '家乡已购房',
|
||||
3: '婚后购房',
|
||||
4: '父母同住',
|
||||
5: '租房',
|
||||
6: '近期有购房计划'
|
||||
}
|
||||
|
||||
const carMap = {
|
||||
1: '已购车',
|
||||
2: '无车',
|
||||
3: '近期购车'
|
||||
}
|
||||
|
||||
const getEducationText = (value) => educationMap[value] || '学历不限'
|
||||
const getIncomeText = (value) => incomeMap[value] || '收入不限'
|
||||
const getMarriageText = (value) => marriageMap[value] || '婚史不限'
|
||||
const getHouseText = (value) => houseMap[value] || '房产不限'
|
||||
const getCarText = (value) => carMap[value] || '车产不限'
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ export const useConfigStore = defineStore('config', {
|
|||
searchBanner: '',
|
||||
butlerQrcode: '', // 管家指导二维码
|
||||
|
||||
// 会员图标配置
|
||||
memberIcons: {
|
||||
unlimitedMemberIcon: '', // 不限时会员图标
|
||||
sincereMemberIcon: '', // 诚意会员图标
|
||||
familyMemberIcon: '' // 家庭版会员图标
|
||||
},
|
||||
|
||||
// 弹窗配置
|
||||
dailyPopup: null,
|
||||
memberAdConfig: null,
|
||||
|
|
@ -53,7 +60,25 @@ export const useConfigStore = defineStore('config', {
|
|||
hasKingKongs: (state) => state.kingKongs.length > 0,
|
||||
hasDailyPopup: (state) => state.dailyPopup !== null,
|
||||
hasMemberAdConfig: (state) => state.memberAdConfig !== null && state.memberAdConfig.status === 1,
|
||||
getDefaultAvatar: (state) => state.defaultAvatar || '/static/logo.png'
|
||||
getDefaultAvatar: (state) => state.defaultAvatar || '/static/logo.png',
|
||||
|
||||
/**
|
||||
* 根据会员等级获取对应的图标URL
|
||||
* @param {number} memberLevel - 会员等级:1不限时会员 2诚意会员 3家庭版会员
|
||||
* @returns {string} 图标URL
|
||||
*/
|
||||
getMemberIcon: (state) => (memberLevel) => {
|
||||
switch (memberLevel) {
|
||||
case 1:
|
||||
return state.memberIcons.unlimitedMemberIcon || ''
|
||||
case 2:
|
||||
return state.memberIcons.sincereMemberIcon || ''
|
||||
case 3:
|
||||
return state.memberIcons.familyMemberIcon || ''
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
@ -80,6 +105,15 @@ export const useConfigStore = defineStore('config', {
|
|||
this.searchBanner = config.searchBanner || ''
|
||||
this.butlerQrcode = config.butlerQrcode || ''
|
||||
|
||||
// 会员图标配置
|
||||
if (config.memberIcons) {
|
||||
this.memberIcons = {
|
||||
unlimitedMemberIcon: config.memberIcons.unlimitedMemberIcon || '',
|
||||
sincereMemberIcon: config.memberIcons.sincereMemberIcon || '',
|
||||
familyMemberIcon: config.memberIcons.familyMemberIcon || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗配置
|
||||
this.dailyPopup = config.dailyPopup || null
|
||||
this.memberAdConfig = config.memberAdPopup || null
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Application.Interfaces;
|
||||
using XiangYi.Application.Services;
|
||||
|
||||
namespace XiangYi.AdminApi.Controllers;
|
||||
|
||||
|
|
@ -201,6 +202,54 @@ public class AdminConfigController : ControllerBase
|
|||
var result = await _configService.SetButlerQrcodeAsync(request.ImageUrl);
|
||||
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取会员图标
|
||||
/// </summary>
|
||||
[HttpGet("memberIcon")]
|
||||
public async Task<ApiResponse<MemberIconResponse>> GetMemberIcon()
|
||||
{
|
||||
var imageUrl = await _configService.GetMemberIconAsync();
|
||||
return ApiResponse<MemberIconResponse>.Success(new MemberIconResponse
|
||||
{
|
||||
ImageUrl = imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置会员图标
|
||||
/// </summary>
|
||||
[HttpPost("memberIcon")]
|
||||
public async Task<ApiResponse> SetMemberIcon([FromBody] SetMemberIconRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ImageUrl))
|
||||
{
|
||||
return ApiResponse.Error(40001, "图片URL不能为空");
|
||||
}
|
||||
|
||||
var result = await _configService.SetMemberIconAsync(request.ImageUrl);
|
||||
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有会员图标
|
||||
/// </summary>
|
||||
[HttpGet("memberIcons")]
|
||||
public async Task<ApiResponse<MemberIconsDto>> GetMemberIcons()
|
||||
{
|
||||
var icons = await _configService.GetMemberIconsAsync();
|
||||
return ApiResponse<MemberIconsDto>.Success(icons);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置所有会员图标
|
||||
/// </summary>
|
||||
[HttpPost("memberIcons")]
|
||||
public async Task<ApiResponse> SetMemberIcons([FromBody] MemberIconsDto request)
|
||||
{
|
||||
var result = await _configService.SetMemberIconsAsync(request);
|
||||
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -312,3 +361,25 @@ public class SetButlerQrcodeRequest
|
|||
/// </summary>
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会员图标响应
|
||||
/// </summary>
|
||||
public class MemberIconResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 图片URL
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置会员图标请求
|
||||
/// </summary>
|
||||
public class SetMemberIconRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 图片URL
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,32 @@ public class ChatController : ControllerBase
|
|||
return ApiResponse<long>.Success(sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除会话
|
||||
/// </summary>
|
||||
/// <param name="request">删除会话请求</param>
|
||||
/// <returns>删除结果</returns>
|
||||
[HttpPost("session/delete")]
|
||||
public async Task<ApiResponse> DeleteSession([FromBody] DeleteSessionRequest request)
|
||||
{
|
||||
if (request.SessionId <= 0)
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.InvalidParameter, "会话ID无效");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// 检查是否有权限访问会话
|
||||
var canAccess = await _chatService.CanAccessSessionAsync(userId, request.SessionId);
|
||||
if (!canAccess)
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.Forbidden, "无权访问该会话");
|
||||
}
|
||||
|
||||
await _chatService.DeleteSessionAsync(userId, request.SessionId);
|
||||
return ApiResponse.Success("删除成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户ID
|
||||
/// </summary>
|
||||
|
|
@ -326,3 +352,14 @@ public class ChatController : ControllerBase
|
|||
return long.TryParse(userIdClaim, out var userId) ? userId : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除会话请求
|
||||
/// </summary>
|
||||
public class DeleteSessionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 会话ID
|
||||
/// </summary>
|
||||
public long SessionId { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,112 @@ public class RealNameController : ControllerBase
|
|||
return ApiResponse<RealNameSubmitResponse>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传身份证图片进行实名认证(小程序使用)
|
||||
/// </summary>
|
||||
/// <param name="frontImage">身份证正面图片</param>
|
||||
/// <param name="backImage">身份证反面图片(可选,可通过单独接口上传)</param>
|
||||
/// <returns>认证结果</returns>
|
||||
[HttpPost("verify")]
|
||||
public async Task<ApiResponse<RealNameVerifyResponse>> VerifyWithImages(
|
||||
IFormFile frontImage,
|
||||
IFormFile? backImage = null)
|
||||
{
|
||||
if (frontImage == null || frontImage.Length == 0)
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.MissingParameter, "请上传身份证正面照片");
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png" };
|
||||
if (!allowedTypes.Contains(frontImage.ContentType.ToLower()))
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.InvalidParameter, "仅支持JPG、PNG格式图片");
|
||||
}
|
||||
|
||||
// 验证文件大小(最大5MB)
|
||||
if (frontImage.Length > 5 * 1024 * 1024)
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.InvalidParameter, "图片大小不能超过5MB");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
try
|
||||
{
|
||||
// 转换为Base64
|
||||
string frontBase64;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
await frontImage.CopyToAsync(ms);
|
||||
frontBase64 = Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
string? backBase64 = null;
|
||||
if (backImage != null && backImage.Length > 0)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await backImage.CopyToAsync(ms);
|
||||
backBase64 = Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
var result = await _realNameService.VerifyWithImagesAsync(userId, frontBase64, backBase64);
|
||||
return ApiResponse<RealNameVerifyResponse>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "实名认证图片上传失败: UserId={UserId}", userId);
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.SystemError, "认证失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单独上传身份证反面图片(用于分步上传)
|
||||
/// </summary>
|
||||
/// <param name="backImage">身份证反面图片</param>
|
||||
/// <returns>上传结果</returns>
|
||||
[HttpPost("verify/back")]
|
||||
public async Task<ApiResponse<RealNameVerifyResponse>> UploadBackImage(IFormFile backImage)
|
||||
{
|
||||
if (backImage == null || backImage.Length == 0)
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.MissingParameter, "请上传身份证反面照片");
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png" };
|
||||
if (!allowedTypes.Contains(backImage.ContentType.ToLower()))
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.InvalidParameter, "仅支持JPG、PNG格式图片");
|
||||
}
|
||||
|
||||
// 验证文件大小(最大5MB)
|
||||
if (backImage.Length > 5 * 1024 * 1024)
|
||||
{
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.InvalidParameter, "图片大小不能超过5MB");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
try
|
||||
{
|
||||
string backBase64;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
await backImage.CopyToAsync(ms);
|
||||
backBase64 = Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
var result = await _realNameService.UploadBackImageAsync(userId, backBase64);
|
||||
return ApiResponse<RealNameVerifyResponse>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证反面上传失败: UserId={UserId}", userId);
|
||||
return ApiResponse<RealNameVerifyResponse>.Error(ErrorCodes.SystemError, "上传失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实名认证状态
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -77,5 +77,10 @@
|
|||
"AccessKeySecret": "",
|
||||
"SignName": "",
|
||||
"TemplateCode": ""
|
||||
},
|
||||
"TencentCloud": {
|
||||
"SecretId": "AKIDVyMfzKZdZP8zkNyOdsFuSsBJDB7EScs0",
|
||||
"SecretKey": "89GWr7JPWYTL8ueHlAYowGZnvzKZjqs9",
|
||||
"Region": "ap-shanghai"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,36 @@ public class ProfileRequest
|
|||
/// </summary>
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否独居
|
||||
/// </summary>
|
||||
public bool IsLivingAlone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母情况
|
||||
/// </summary>
|
||||
public string? ParentStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母是否退休
|
||||
/// </summary>
|
||||
public bool IsParentRetired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居省份
|
||||
/// </summary>
|
||||
public string? ParentProvince { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居城市
|
||||
/// </summary>
|
||||
public string? ParentCity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母是否有房
|
||||
/// </summary>
|
||||
public bool ParentHasHouse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 择偶要求
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@ public class ProfileResponse
|
|||
/// </summary>
|
||||
public bool IsParentRetired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居省份
|
||||
/// </summary>
|
||||
public string? ParentProvince { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居城市
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,48 @@ public class RealNameSubmitResponse
|
|||
public string? MaskedIdCard { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实名认证图片验证响应
|
||||
/// </summary>
|
||||
public class RealNameVerifyResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否成功
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否需要上传反面(分步上传时使用)
|
||||
/// </summary>
|
||||
public bool NeedBackImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 认证数据(成功时返回)
|
||||
/// </summary>
|
||||
public RealNameVerifyData? Data { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实名认证数据
|
||||
/// </summary>
|
||||
public class RealNameVerifyData
|
||||
{
|
||||
/// <summary>
|
||||
/// 脱敏后的姓名
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏后的身份证号
|
||||
/// </summary>
|
||||
public string IdNumber { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实名认证状态响应
|
||||
/// </summary>
|
||||
|
|
@ -72,6 +114,11 @@ public class RealNameStatusResponse
|
|||
/// </summary>
|
||||
public bool IsRealName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已支付(用于判断是否可以上传身份证)
|
||||
/// </summary>
|
||||
public bool IsPaid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 认证状态:0未认证 1待审核 2已通过 3已拒绝
|
||||
/// </summary>
|
||||
|
|
@ -111,4 +158,14 @@ public class RealNameStatusResponse
|
|||
/// 认证费用
|
||||
/// </summary>
|
||||
public decimal? PaymentAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名(用于前端显示)
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 身份证号(用于前端显示)
|
||||
/// </summary>
|
||||
public string? IdNumber { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,11 @@ public class RecommendUserResponse
|
|||
/// </summary>
|
||||
public bool IsMember { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员等级:0非会员 1不限时会员 2诚意会员 3家庭版会员
|
||||
/// </summary>
|
||||
public int MemberLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否实名认证
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -85,4 +85,12 @@ public interface IChatService
|
|||
/// <param name="sessionId">会话ID</param>
|
||||
/// <returns>是否有权限</returns>
|
||||
Task<bool> CanAccessSessionAsync(long userId, long sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// 删除会话(软删除,仅对当前用户隐藏)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="sessionId">会话ID</param>
|
||||
/// <returns></returns>
|
||||
Task DeleteSessionAsync(long userId, long sessionId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using XiangYi.Application.Services;
|
||||
|
||||
namespace XiangYi.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -94,6 +96,11 @@ public class AppConfigResponse
|
|||
/// </summary>
|
||||
public string? ButlerQrcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员图标配置
|
||||
/// </summary>
|
||||
public MemberIconsDto? MemberIcons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每日弹窗配置
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,23 @@ public interface IRealNameService
|
|||
/// <returns>认证结果</returns>
|
||||
Task<RealNameSubmitResponse> SubmitRealNameAsync(long userId, RealNameSubmitRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 通过身份证图片进行实名认证
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="frontImageBase64">身份证正面图片Base64</param>
|
||||
/// <param name="backImageBase64">身份证反面图片Base64(可选)</param>
|
||||
/// <returns>认证结果</returns>
|
||||
Task<RealNameVerifyResponse> VerifyWithImagesAsync(long userId, string frontImageBase64, string? backImageBase64);
|
||||
|
||||
/// <summary>
|
||||
/// 上传身份证反面图片(分步上传)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="backImageBase64">身份证反面图片Base64</param>
|
||||
/// <returns>认证结果</returns>
|
||||
Task<RealNameVerifyResponse> UploadBackImageAsync(long userId, string backImageBase64);
|
||||
|
||||
/// <summary>
|
||||
/// 获取实名认证状态
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using XiangYi.Application.Services;
|
||||
|
||||
namespace XiangYi.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -79,4 +81,24 @@ public interface ISystemConfigService
|
|||
/// 设置管家指导二维码URL
|
||||
/// </summary>
|
||||
Task<bool> SetButlerQrcodeAsync(string imageUrl);
|
||||
|
||||
/// <summary>
|
||||
/// 获取会员图标URL(已废弃,请使用GetMemberIconsAsync)
|
||||
/// </summary>
|
||||
Task<string?> GetMemberIconAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 设置会员图标URL(已废弃,请使用SetMemberIconsAsync)
|
||||
/// </summary>
|
||||
Task<bool> SetMemberIconAsync(string imageUrl);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有会员图标
|
||||
/// </summary>
|
||||
Task<MemberIconsDto> GetMemberIconsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 设置所有会员图标
|
||||
/// </summary>
|
||||
Task<bool> SetMemberIconsAsync(MemberIconsDto icons);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@ public class ChatService : IChatService
|
|||
/// <inheritdoc />
|
||||
public async Task<List<ChatSessionResponse>> GetSessionsAsync(long userId)
|
||||
{
|
||||
// 获取用户参与的所有会话
|
||||
// 获取用户参与的所有会话,排除已删除的
|
||||
var sessions = await _sessionRepository.GetListAsync(s =>
|
||||
s.User1Id == userId || s.User2Id == userId);
|
||||
(s.User1Id == userId && !s.User1Deleted) || (s.User2Id == userId && !s.User2Deleted));
|
||||
|
||||
// 按最后消息时间排序
|
||||
sessions = sessions.OrderByDescending(s => s.LastMessageTime ?? s.CreateTime).ToList();
|
||||
|
|
@ -191,6 +191,38 @@ public class ChatService : IChatService
|
|||
return IsUserInSession(userId, session);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteSessionAsync(long userId, long sessionId)
|
||||
{
|
||||
var session = await _sessionRepository.GetByIdAsync(sessionId);
|
||||
if (session == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.SessionNotFound, "会话不存在");
|
||||
}
|
||||
|
||||
if (!IsUserInSession(userId, session))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.SessionAccessDenied, "无权访问该会话");
|
||||
}
|
||||
|
||||
// 软删除:标记该用户已删除此会话
|
||||
// 如果是用户1删除,设置User1Deleted = true
|
||||
// 如果是用户2删除,设置User2Deleted = true
|
||||
if (session.User1Id == userId)
|
||||
{
|
||||
session.User1Deleted = true;
|
||||
}
|
||||
else if (session.User2Id == userId)
|
||||
{
|
||||
session.User2Deleted = true;
|
||||
}
|
||||
|
||||
session.UpdateTime = DateTime.Now;
|
||||
await _sessionRepository.UpdateAsync(session);
|
||||
|
||||
_logger.LogInformation("用户删除会话: UserId={UserId}, SessionId={SessionId}", userId, sessionId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 消息管理
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public class ConfigService : IConfigService
|
|||
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync();
|
||||
var searchBanner = await _systemConfigService.GetSearchBannerAsync();
|
||||
var butlerQrcode = await _systemConfigService.GetButlerQrcodeAsync();
|
||||
var memberIcons = await _systemConfigService.GetMemberIconsAsync();
|
||||
var dailyPopup = await GetPopupConfigAsync(1); // 每日弹窗
|
||||
var memberAdPopup = await GetPopupConfigAsync(3); // 会员广告弹窗
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ public class ConfigService : IConfigService
|
|||
DefaultAvatar = defaultAvatar,
|
||||
SearchBanner = searchBanner,
|
||||
ButlerQrcode = butlerQrcode,
|
||||
MemberIcons = memberIcons,
|
||||
DailyPopup = dailyPopup,
|
||||
MemberAdPopup = memberAdPopup
|
||||
};
|
||||
|
|
|
|||
|
|
@ -95,6 +95,13 @@ public class ProfileService : IProfileService
|
|||
HomeProvince = request.HomeProvince,
|
||||
HomeCity = request.HomeCity,
|
||||
WeChatNo = request.WeChatNo,
|
||||
// 父母情况
|
||||
IsLivingAlone = request.IsLivingAlone,
|
||||
ParentStatus = request.ParentStatus,
|
||||
IsParentRetired = request.IsParentRetired,
|
||||
ParentProvince = request.ParentProvince,
|
||||
ParentCity = request.ParentCity,
|
||||
ParentHasHouse = request.ParentHasHouse,
|
||||
AuditStatus = (int)AuditStatus.Pending, // 新资料设为待审核
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
|
|
@ -129,6 +136,13 @@ public class ProfileService : IProfileService
|
|||
existingProfile.HomeProvince = request.HomeProvince;
|
||||
existingProfile.HomeCity = request.HomeCity;
|
||||
existingProfile.WeChatNo = request.WeChatNo;
|
||||
// 父母情况
|
||||
existingProfile.IsLivingAlone = request.IsLivingAlone;
|
||||
existingProfile.ParentStatus = request.ParentStatus;
|
||||
existingProfile.IsParentRetired = request.IsParentRetired;
|
||||
existingProfile.ParentProvince = request.ParentProvince;
|
||||
existingProfile.ParentCity = request.ParentCity;
|
||||
existingProfile.ParentHasHouse = request.ParentHasHouse;
|
||||
existingProfile.AuditStatus = (int)AuditStatus.Pending; // 更新资料重置为待审核
|
||||
existingProfile.RejectReason = null; // 清除之前的拒绝原因
|
||||
existingProfile.UpdateTime = DateTime.Now;
|
||||
|
|
@ -238,6 +252,13 @@ public class ProfileService : IProfileService
|
|||
WeChatNo = profile.WeChatNo,
|
||||
AuditStatus = profile.AuditStatus,
|
||||
RejectReason = profile.RejectReason,
|
||||
// 父母情况
|
||||
IsLivingAlone = profile.IsLivingAlone,
|
||||
ParentStatus = profile.ParentStatus,
|
||||
IsParentRetired = profile.IsParentRetired,
|
||||
ParentProvince = profile.ParentProvince,
|
||||
ParentCity = profile.ParentCity,
|
||||
ParentHasHouse = profile.ParentHasHouse,
|
||||
IsMember = user.IsMember,
|
||||
IsRealName = user.IsRealName,
|
||||
Photos = photos.OrderBy(p => p.Sort).Select(p => new PhotoResponse
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using XiangYi.Core.Exceptions;
|
|||
using XiangYi.Core.Interfaces;
|
||||
using XiangYi.Infrastructure.RealName;
|
||||
using XiangYi.Infrastructure.WeChat;
|
||||
using XiangYi.Infrastructure.Cache;
|
||||
|
||||
namespace XiangYi.Application.Services;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ public class RealNameService : IRealNameService
|
|||
private readonly IRepository<RealNameAuth> _realNameAuthRepository;
|
||||
private readonly IWeChatService _weChatService;
|
||||
private readonly IRealNameProvider _realNameProvider;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ILogger<RealNameService> _logger;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,6 +31,16 @@ public class RealNameService : IRealNameService
|
|||
/// </summary>
|
||||
private const decimal RealNamePrice = 88m;
|
||||
|
||||
/// <summary>
|
||||
/// 临时OCR数据缓存前缀
|
||||
/// </summary>
|
||||
private const string OcrCachePrefix = "realname:ocr:";
|
||||
|
||||
/// <summary>
|
||||
/// 临时OCR数据缓存时间(分钟)
|
||||
/// </summary>
|
||||
private const int OcrCacheMinutes = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 认证状态名称
|
||||
/// </summary>
|
||||
|
|
@ -46,6 +58,7 @@ public class RealNameService : IRealNameService
|
|||
IRepository<RealNameAuth> realNameAuthRepository,
|
||||
IWeChatService weChatService,
|
||||
IRealNameProvider realNameProvider,
|
||||
ICacheService cacheService,
|
||||
ILogger<RealNameService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
|
|
@ -53,6 +66,7 @@ public class RealNameService : IRealNameService
|
|||
_realNameAuthRepository = realNameAuthRepository;
|
||||
_weChatService = weChatService;
|
||||
_realNameProvider = realNameProvider;
|
||||
_cacheService = cacheService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -265,6 +279,18 @@ public class RealNameService : IRealNameService
|
|||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 检查是否已支付实名认证费用
|
||||
var hasPaidOrder = false;
|
||||
if (!user.IsMember)
|
||||
{
|
||||
var paidOrder = (await _orderRepository.GetListAsync(
|
||||
o => o.UserId == userId &&
|
||||
o.OrderType == (int)OrderType.RealName &&
|
||||
o.Status == (int)OrderStatus.Paid))
|
||||
.FirstOrDefault();
|
||||
hasPaidOrder = paidOrder != null;
|
||||
}
|
||||
|
||||
// 查找最新的实名认证记录
|
||||
var authRecords = await _realNameAuthRepository.GetListAsync(a => a.UserId == userId);
|
||||
var latestAuth = authRecords.OrderByDescending(a => a.CreateTime).FirstOrDefault();
|
||||
|
|
@ -275,9 +301,10 @@ public class RealNameService : IRealNameService
|
|||
return new RealNameStatusResponse
|
||||
{
|
||||
IsRealName = false,
|
||||
IsPaid = user.IsMember || hasPaidOrder,
|
||||
Status = 0,
|
||||
StatusName = GetStatusName(0),
|
||||
NeedPayment = !user.IsMember,
|
||||
NeedPayment = !user.IsMember && !hasPaidOrder,
|
||||
PaymentAmount = user.IsMember ? null : RealNamePrice
|
||||
};
|
||||
}
|
||||
|
|
@ -285,10 +312,13 @@ public class RealNameService : IRealNameService
|
|||
return new RealNameStatusResponse
|
||||
{
|
||||
IsRealName = user.IsRealName,
|
||||
IsPaid = user.IsMember || hasPaidOrder,
|
||||
Status = latestAuth.Status,
|
||||
StatusName = GetStatusName(latestAuth.Status),
|
||||
MaskedName = latestAuth.RealName,
|
||||
MaskedIdCard = latestAuth.IdCard,
|
||||
Name = latestAuth.RealName,
|
||||
IdNumber = latestAuth.IdCard,
|
||||
RejectReason = latestAuth.RejectReason,
|
||||
VerifyTime = latestAuth.VerifyTime,
|
||||
NeedPayment = !user.IsMember && latestAuth.Status != (int)RealNameAuthStatus.Approved,
|
||||
|
|
@ -296,6 +326,222 @@ public class RealNameService : IRealNameService
|
|||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RealNameVerifyResponse> VerifyWithImagesAsync(long userId, string frontImageBase64, string? backImageBase64)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 已实名用户不能重复认证
|
||||
if (user.IsRealName)
|
||||
{
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "您已完成实名认证"
|
||||
};
|
||||
}
|
||||
|
||||
// 非会员用户需要检查是否已支付
|
||||
if (!user.IsMember)
|
||||
{
|
||||
var paidOrder = (await _orderRepository.GetListAsync(
|
||||
o => o.UserId == userId &&
|
||||
o.OrderType == (int)OrderType.RealName &&
|
||||
o.Status == (int)OrderStatus.Paid))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (paidOrder == null)
|
||||
{
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "请先完成实名认证费用支付"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// OCR识别身份证正面
|
||||
var frontOcrResult = await _realNameProvider.OcrIdCardFrontAsync(frontImageBase64);
|
||||
if (!frontOcrResult.Success)
|
||||
{
|
||||
_logger.LogWarning("身份证正面OCR识别失败: UserId={UserId}, Error={Error}", userId, frontOcrResult.ErrorMessage);
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = frontOcrResult.ErrorMessage ?? "身份证正面识别失败,请重新拍照"
|
||||
};
|
||||
}
|
||||
|
||||
var name = frontOcrResult.Name!;
|
||||
var idNumber = frontOcrResult.IdNumber!;
|
||||
|
||||
// 如果没有反面图片,需要分步上传
|
||||
if (string.IsNullOrEmpty(backImageBase64))
|
||||
{
|
||||
// 缓存正面OCR结果,等待反面上传
|
||||
var cacheKey = $"{OcrCachePrefix}{userId}";
|
||||
await _cacheService.SetAsync(cacheKey, new OcrCacheData
|
||||
{
|
||||
Name = name,
|
||||
IdNumber = idNumber
|
||||
}, OcrCacheMinutes * 60);
|
||||
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "身份证正面识别成功,请上传反面",
|
||||
NeedBackImage = true
|
||||
};
|
||||
}
|
||||
|
||||
// OCR识别身份证反面
|
||||
var backOcrResult = await _realNameProvider.OcrIdCardBackAsync(backImageBase64);
|
||||
if (!backOcrResult.Success)
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别失败: UserId={UserId}, Error={Error}", userId, backOcrResult.ErrorMessage);
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = backOcrResult.ErrorMessage ?? "身份证反面识别失败,请重新拍照"
|
||||
};
|
||||
}
|
||||
|
||||
// 调用实名认证接口验证
|
||||
return await CompleteVerificationAsync(userId, user, name, idNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RealNameVerifyResponse> UploadBackImageAsync(long userId, string backImageBase64)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 已实名用户不能重复认证
|
||||
if (user.IsRealName)
|
||||
{
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "您已完成实名认证"
|
||||
};
|
||||
}
|
||||
|
||||
// 获取缓存的正面OCR数据
|
||||
var cacheKey = $"{OcrCachePrefix}{userId}";
|
||||
var cachedData = await _cacheService.GetAsync<OcrCacheData>(cacheKey);
|
||||
if (cachedData == null)
|
||||
{
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "请先上传身份证正面"
|
||||
};
|
||||
}
|
||||
|
||||
// OCR识别身份证反面
|
||||
var backOcrResult = await _realNameProvider.OcrIdCardBackAsync(backImageBase64);
|
||||
if (!backOcrResult.Success)
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别失败: UserId={UserId}, Error={Error}", userId, backOcrResult.ErrorMessage);
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = backOcrResult.ErrorMessage ?? "身份证反面识别失败,请重新拍照"
|
||||
};
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
await _cacheService.RemoveAsync(cacheKey);
|
||||
|
||||
// 调用实名认证接口验证
|
||||
return await CompleteVerificationAsync(userId, user, cachedData.Name, cachedData.IdNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完成实名认证验证
|
||||
/// </summary>
|
||||
private async Task<RealNameVerifyResponse> CompleteVerificationAsync(long userId, User user, string name, string idNumber)
|
||||
{
|
||||
// 调用第三方实名认证接口验证
|
||||
var verifyResult = await _realNameProvider.VerifyIdCardAsync(name, idNumber);
|
||||
|
||||
if (!verifyResult.IsVerified)
|
||||
{
|
||||
_logger.LogWarning("实名认证验证失败: UserId={UserId}, Error={Error}", userId, verifyResult.ErrorMessage);
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = verifyResult.ErrorMessage ?? "身份信息验证失败,请确认身份证信息正确"
|
||||
};
|
||||
}
|
||||
|
||||
// 脱敏存储身份信息
|
||||
var maskedName = IRealNameService.MaskName(name);
|
||||
var maskedIdCard = IRealNameService.MaskIdCard(idNumber);
|
||||
|
||||
// 查找关联订单
|
||||
long orderId = 0;
|
||||
if (!user.IsMember)
|
||||
{
|
||||
var order = (await _orderRepository.GetListAsync(
|
||||
o => o.UserId == userId &&
|
||||
o.OrderType == (int)OrderType.RealName &&
|
||||
o.Status == (int)OrderStatus.Paid))
|
||||
.FirstOrDefault();
|
||||
orderId = order?.Id ?? 0;
|
||||
}
|
||||
|
||||
// 创建实名认证记录
|
||||
var realNameAuth = new RealNameAuth
|
||||
{
|
||||
UserId = userId,
|
||||
OrderId = orderId,
|
||||
RealName = maskedName,
|
||||
IdCard = maskedIdCard,
|
||||
Status = (int)RealNameAuthStatus.Approved,
|
||||
VerifyTime = DateTime.Now,
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
};
|
||||
|
||||
await _realNameAuthRepository.AddAsync(realNameAuth);
|
||||
|
||||
// 更新用户实名状态
|
||||
user.IsRealName = true;
|
||||
user.UpdateTime = DateTime.Now;
|
||||
await _userRepository.UpdateAsync(user);
|
||||
|
||||
_logger.LogInformation("实名认证成功: UserId={UserId}", userId);
|
||||
|
||||
return new RealNameVerifyResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "实名认证成功",
|
||||
NeedBackImage = false,
|
||||
Data = new RealNameVerifyData
|
||||
{
|
||||
Name = maskedName,
|
||||
IdNumber = maskedIdCard
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OCR缓存数据
|
||||
/// </summary>
|
||||
private class OcrCacheData
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string IdNumber { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#region 私有方法
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ public class RecommendService : IRecommendService
|
|||
MonthlyIncome = profile.MonthlyIncome,
|
||||
Intro = profile.Introduction,
|
||||
IsMember = recommendUser.IsMember,
|
||||
MemberLevel = recommendUser.MemberLevel,
|
||||
IsRealName = recommendUser.IsRealName,
|
||||
IsPhotoPublic = profile.IsPhotoPublic,
|
||||
FirstPhoto = firstPhoto,
|
||||
|
|
@ -232,6 +233,7 @@ public class RecommendService : IRecommendService
|
|||
MonthlyIncome = profile.MonthlyIncome,
|
||||
Intro = profile.Introduction,
|
||||
IsMember = user.IsMember,
|
||||
MemberLevel = user.MemberLevel,
|
||||
IsRealName = user.IsRealName,
|
||||
IsPhotoPublic = profile.IsPhotoPublic,
|
||||
FirstPhoto = firstPhoto,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,26 @@ public class SystemConfigService : ISystemConfigService
|
|||
/// </summary>
|
||||
public const string ButlerQrcodeKey = "butler_qrcode";
|
||||
|
||||
/// <summary>
|
||||
/// 会员图标配置键
|
||||
/// </summary>
|
||||
public const string MemberIconKey = "member_icon";
|
||||
|
||||
/// <summary>
|
||||
/// 不限时会员图标配置键
|
||||
/// </summary>
|
||||
public const string UnlimitedMemberIconKey = "unlimited_member_icon";
|
||||
|
||||
/// <summary>
|
||||
/// 诚意会员图标配置键
|
||||
/// </summary>
|
||||
public const string SincereMemberIconKey = "sincere_member_icon";
|
||||
|
||||
/// <summary>
|
||||
/// 家庭版会员图标配置键
|
||||
/// </summary>
|
||||
public const string FamilyMemberIconKey = "family_member_icon";
|
||||
|
||||
public SystemConfigService(
|
||||
IRepository<SystemConfig> configRepository,
|
||||
ILogger<SystemConfigService> logger)
|
||||
|
|
@ -177,4 +197,78 @@ public class SystemConfigService : ISystemConfigService
|
|||
{
|
||||
return await SetConfigValueAsync(ButlerQrcodeKey, imageUrl, "管家指导二维码URL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetMemberIconAsync()
|
||||
{
|
||||
return await GetConfigValueAsync(MemberIconKey);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetMemberIconAsync(string imageUrl)
|
||||
{
|
||||
return await SetConfigValueAsync(MemberIconKey, imageUrl, "会员图标URL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberIconsDto> GetMemberIconsAsync()
|
||||
{
|
||||
var unlimited = await GetConfigValueAsync(UnlimitedMemberIconKey);
|
||||
var sincere = await GetConfigValueAsync(SincereMemberIconKey);
|
||||
var family = await GetConfigValueAsync(FamilyMemberIconKey);
|
||||
|
||||
return new MemberIconsDto
|
||||
{
|
||||
UnlimitedMemberIcon = unlimited,
|
||||
SincereMemberIcon = sincere,
|
||||
FamilyMemberIcon = family
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetMemberIconsAsync(MemberIconsDto icons)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(icons.UnlimitedMemberIcon))
|
||||
{
|
||||
await SetConfigValueAsync(UnlimitedMemberIconKey, icons.UnlimitedMemberIcon, "不限时会员图标URL");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(icons.SincereMemberIcon))
|
||||
{
|
||||
await SetConfigValueAsync(SincereMemberIconKey, icons.SincereMemberIcon, "诚意会员图标URL");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(icons.FamilyMemberIcon))
|
||||
{
|
||||
await SetConfigValueAsync(FamilyMemberIconKey, icons.FamilyMemberIcon, "家庭版会员图标URL");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "设置会员图标失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会员图标DTO
|
||||
/// </summary>
|
||||
public class MemberIconsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 不限时会员图标URL
|
||||
/// </summary>
|
||||
public string? UnlimitedMemberIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 诚意会员图标URL
|
||||
/// </summary>
|
||||
public string? SincereMemberIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 家庭版会员图标URL
|
||||
/// </summary>
|
||||
public string? FamilyMemberIcon { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ public class ChatSession : BaseEntity
|
|||
/// </summary>
|
||||
public int User2UnreadCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 用户1是否已删除会话
|
||||
/// </summary>
|
||||
public bool User1Deleted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 用户2是否已删除会话
|
||||
/// </summary>
|
||||
public bool User2Deleted { get; set; } = false;
|
||||
|
||||
#region 导航属性
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -148,6 +148,39 @@ public class UserProfile : BaseEntity
|
|||
[Column(StringLength = 200)]
|
||||
public string? RejectReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否独居
|
||||
/// </summary>
|
||||
public bool IsLivingAlone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母情况
|
||||
/// </summary>
|
||||
[Column(StringLength = 50)]
|
||||
public string? ParentStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母是否退休
|
||||
/// </summary>
|
||||
public bool IsParentRetired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居省份
|
||||
/// </summary>
|
||||
[Column(StringLength = 20)]
|
||||
public string? ParentProvince { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母现居城市
|
||||
/// </summary>
|
||||
[Column(StringLength = 20)]
|
||||
public string? ParentCity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父母是否有房
|
||||
/// </summary>
|
||||
public bool ParentHasHouse { get; set; }
|
||||
|
||||
#region 导航属性
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,123 @@ public interface IRealNameProvider
|
|||
/// <param name="photoBase64">人脸照片Base64</param>
|
||||
/// <returns>验证结果</returns>
|
||||
Task<RealNameResult> VerifyWithPhotoAsync(string name, string idCard, string photoBase64);
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(正面)
|
||||
/// </summary>
|
||||
/// <param name="imageBase64">身份证正面照片Base64</param>
|
||||
/// <returns>OCR识别结果</returns>
|
||||
Task<IdCardOcrResult> OcrIdCardFrontAsync(string imageBase64);
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(反面)
|
||||
/// </summary>
|
||||
/// <param name="imageBase64">身份证反面照片Base64</param>
|
||||
/// <returns>OCR识别结果</returns>
|
||||
Task<IdCardOcrResult> OcrIdCardBackAsync(string imageBase64);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别结果
|
||||
/// </summary>
|
||||
public class IdCardOcrResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否识别成功
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名(正面)
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 身份证号(正面)
|
||||
/// </summary>
|
||||
public string? IdNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 性别(正面)
|
||||
/// </summary>
|
||||
public string? Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 民族(正面)
|
||||
/// </summary>
|
||||
public string? Nation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出生日期(正面)
|
||||
/// </summary>
|
||||
public string? Birth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 住址(正面)
|
||||
/// </summary>
|
||||
public string? Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签发机关(反面)
|
||||
/// </summary>
|
||||
public string? Authority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期限(反面)
|
||||
/// </summary>
|
||||
public string? ValidDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误码
|
||||
/// </summary>
|
||||
public string? ErrorCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求ID
|
||||
/// </summary>
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public static IdCardOcrResult SuccessFront(string name, string idNumber, string? gender, string? nation, string? birth, string? address, string? requestId = null)
|
||||
{
|
||||
return new IdCardOcrResult
|
||||
{
|
||||
Success = true,
|
||||
Name = name,
|
||||
IdNumber = idNumber,
|
||||
Gender = gender,
|
||||
Nation = nation,
|
||||
Birth = birth,
|
||||
Address = address,
|
||||
RequestId = requestId
|
||||
};
|
||||
}
|
||||
|
||||
public static IdCardOcrResult SuccessBack(string authority, string validDate, string? requestId = null)
|
||||
{
|
||||
return new IdCardOcrResult
|
||||
{
|
||||
Success = true,
|
||||
Authority = authority,
|
||||
ValidDate = validDate,
|
||||
RequestId = requestId
|
||||
};
|
||||
}
|
||||
|
||||
public static IdCardOcrResult Fail(string errorCode, string errorMessage, string? requestId = null)
|
||||
{
|
||||
return new IdCardOcrResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorCode = errorCode,
|
||||
ErrorMessage = errorMessage,
|
||||
RequestId = requestId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -116,6 +116,158 @@ public class TencentRealNameProvider : IRealNameProvider
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(正面)
|
||||
/// </summary>
|
||||
public async Task<IdCardOcrResult> OcrIdCardFrontAsync(string imageBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
ImageBase64 = imageBase64,
|
||||
CardSide = "FRONT"
|
||||
};
|
||||
|
||||
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
|
||||
|
||||
if (response?.Response?.Error != null)
|
||||
{
|
||||
_logger.LogWarning("身份证正面OCR识别失败: {Error}", response.Response.Error.Message);
|
||||
return IdCardOcrResult.Fail(
|
||||
response.Response.Error.Code ?? "UNKNOWN",
|
||||
response.Response.Error.Message ?? "识别失败",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(response?.Response?.Name) || string.IsNullOrEmpty(response?.Response?.IdNum))
|
||||
{
|
||||
_logger.LogWarning("身份证正面OCR识别结果为空");
|
||||
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
|
||||
}
|
||||
|
||||
_logger.LogInformation("身份证正面OCR识别成功: {Name}", MaskName(response.Response.Name));
|
||||
return IdCardOcrResult.SuccessFront(
|
||||
response.Response.Name,
|
||||
response.Response.IdNum,
|
||||
response.Response.Sex,
|
||||
response.Response.Nation,
|
||||
response.Response.Birth,
|
||||
response.Response.Address,
|
||||
response.Response.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证正面OCR识别异常");
|
||||
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身份证OCR识别(反面)
|
||||
/// </summary>
|
||||
public async Task<IdCardOcrResult> OcrIdCardBackAsync(string imageBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
ImageBase64 = imageBase64,
|
||||
CardSide = "BACK"
|
||||
};
|
||||
|
||||
var response = await CallOcrApiAsync("IDCardOCR", requestBody);
|
||||
|
||||
if (response?.Response?.Error != null)
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别失败: {Error}", response.Response.Error.Message);
|
||||
return IdCardOcrResult.Fail(
|
||||
response.Response.Error.Code ?? "UNKNOWN",
|
||||
response.Response.Error.Message ?? "识别失败",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(response?.Response?.Authority))
|
||||
{
|
||||
_logger.LogWarning("身份证反面OCR识别结果为空");
|
||||
return IdCardOcrResult.Fail("EMPTY_RESULT", "未能识别到身份证信息,请重新拍照");
|
||||
}
|
||||
|
||||
_logger.LogInformation("身份证反面OCR识别成功");
|
||||
return IdCardOcrResult.SuccessBack(
|
||||
response.Response.Authority,
|
||||
response.Response.ValidDate ?? "",
|
||||
response.Response.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "身份证反面OCR识别异常");
|
||||
return IdCardOcrResult.Fail("EXCEPTION", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TencentOcrApiResponse?> CallOcrApiAsync(string action, object requestBody)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
var payload = JsonSerializer.Serialize(requestBody);
|
||||
|
||||
// OCR使用不同的服务域名
|
||||
var ocrHost = "ocr.tencentcloudapi.com";
|
||||
var ocrService = "ocr";
|
||||
var authorization = GenerateAuthorizationForOcr(action, timestamp, date, payload, ocrHost, ocrService);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"https://{ocrHost}/")
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
request.Headers.Add("Authorization", authorization);
|
||||
request.Headers.Add("X-TC-Action", action);
|
||||
request.Headers.Add("X-TC-Version", "2018-11-19"); // OCR API版本
|
||||
request.Headers.Add("X-TC-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-TC-Region", _options.Region);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonSerializer.Deserialize<TencentOcrApiResponse>(content, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateAuthorizationForOcr(string action, long timestamp, string date, string payload, string host, string service)
|
||||
{
|
||||
var algorithm = "TC3-HMAC-SHA256";
|
||||
var httpRequestMethod = "POST";
|
||||
var canonicalUri = "/";
|
||||
var canonicalQueryString = "";
|
||||
var canonicalHeaders = $"content-type:application/json; charset=utf-8\nhost:{host}\n";
|
||||
var signedHeaders = "content-type;host";
|
||||
var hashedRequestPayload = Sha256Hex(payload);
|
||||
|
||||
var canonicalRequest = $"{httpRequestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
|
||||
|
||||
var credentialScope = $"{date}/{service}/tc3_request";
|
||||
var hashedCanonicalRequest = Sha256Hex(canonicalRequest);
|
||||
var stringToSign = $"{algorithm}\n{timestamp}\n{credentialScope}\n{hashedCanonicalRequest}";
|
||||
|
||||
var secretDate = HmacSha256($"TC3{_options.SecretKey}", date);
|
||||
var secretService = HmacSha256(secretDate, service);
|
||||
var secretSigning = HmacSha256(secretService, "tc3_request");
|
||||
var signature = HmacSha256Hex(secretSigning, stringToSign);
|
||||
|
||||
return $"{algorithm} Credential={_options.SecretId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
|
||||
}
|
||||
|
||||
private static string MaskName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || name.Length < 2)
|
||||
return "***";
|
||||
return $"{name[0]}**";
|
||||
}
|
||||
|
||||
private async Task<TencentApiResponse?> CallApiAsync(string action, object requestBody)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
|
@ -216,4 +368,30 @@ public class TencentRealNameProvider : IRealNameProvider
|
|||
public string? Code { get; set; }
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云OCR API响应
|
||||
/// </summary>
|
||||
private class TencentOcrApiResponse
|
||||
{
|
||||
public TencentOcrResponseData? Response { get; set; }
|
||||
}
|
||||
|
||||
private class TencentOcrResponseData
|
||||
{
|
||||
// 正面信息
|
||||
public string? Name { get; set; }
|
||||
public string? Sex { get; set; }
|
||||
public string? Nation { get; set; }
|
||||
public string? Birth { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? IdNum { get; set; }
|
||||
|
||||
// 反面信息
|
||||
public string? Authority { get; set; }
|
||||
public string? ValidDate { get; set; }
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
public TencentApiError? Error { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user