Merge branch 'master' of 192.168.195.14:outsource/xiangyixiangqin

This commit is contained in:
zpc 2026-02-28 19:16:45 +08:00
commit 91f22008ed
18 changed files with 498 additions and 228 deletions

View File

@ -77,6 +77,20 @@ export function setSearchBanner(imageUrl: string) {
return request.post('/admin/config/searchBanner', { imageUrl })
}
/**
* Banner
*/
export function getRealNameBanner() {
return request.get('/admin/config/realNameBanner')
}
/**
* Banner
*/
export function setRealNameBanner(imageUrl: string) {
return request.post('/admin/config/realNameBanner', { imageUrl })
}
/**
*
*/

View File

@ -57,6 +57,29 @@
</div>
</el-form-item>
<!-- 实名认证页Banner设置 -->
<el-form-item label="实名认证Banner">
<div class="banner-upload">
<el-upload
class="banner-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleRealNameBannerSuccess"
:before-upload="beforeAvatarUpload"
accept="image/*"
>
<img v-if="configForm.realNameBanner" :src="getFullUrl(configForm.realNameBanner)" class="banner-preview" />
<el-icon v-else class="banner-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="avatar-tip">
<p>建议尺寸750x400像素</p>
<p>支持格式JPGPNG</p>
<p>小程序实名认证页顶部展示的Banner图片</p>
</div>
</div>
</el-form-item>
<!-- 管家二维码设置 -->
<el-form-item label="管家二维码">
<div class="qrcode-upload">
@ -274,6 +297,8 @@ import {
setPrivacyPolicy,
getSearchBanner,
setSearchBanner,
getRealNameBanner,
setRealNameBanner,
getButlerQrcode,
setButlerQrcode,
getMemberIcons,
@ -294,6 +319,7 @@ const activeTab = ref('basic')
const configForm = ref({
defaultAvatar: '',
searchBanner: '',
realNameBanner: '',
butlerQrcode: '',
unlimitedMemberIcon: '',
sincereMemberIcon: '',
@ -326,9 +352,10 @@ const getFullUrl = (url) => {
const loadConfig = async () => {
try {
const [avatarRes, bannerRes, qrcodeRes, memberIconsRes, memberEntryRes, realNamePriceRes] = await Promise.all([
const [avatarRes, bannerRes, realNameBannerRes, qrcodeRes, memberIconsRes, memberEntryRes, realNamePriceRes] = await Promise.all([
getDefaultAvatar(),
getSearchBanner(),
getRealNameBanner(),
getButlerQrcode(),
getMemberIcons(),
getMemberEntryImage(),
@ -340,6 +367,9 @@ const loadConfig = async () => {
if (bannerRes) {
configForm.value.searchBanner = bannerRes.imageUrl || ''
}
if (realNameBannerRes) {
configForm.value.realNameBanner = realNameBannerRes.imageUrl || ''
}
if (qrcodeRes) {
configForm.value.butlerQrcode = qrcodeRes.imageUrl || ''
}
@ -397,6 +427,15 @@ const handleBannerSuccess = (response) => {
}
}
const handleRealNameBannerSuccess = (response) => {
if (response.code === 0 && response.data) {
configForm.value.realNameBanner = response.data.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message || '上传失败')
}
}
const handleQrcodeSuccess = (response) => {
if (response.code === 0 && response.data) {
configForm.value.butlerQrcode = response.data.url
@ -476,6 +515,9 @@ const saveBasicConfig = async () => {
if (configForm.value.searchBanner) {
promises.push(setSearchBanner(configForm.value.searchBanner))
}
if (configForm.value.realNameBanner) {
promises.push(setRealNameBanner(configForm.value.realNameBanner))
}
if (configForm.value.butlerQrcode) {
promises.push(setButlerQrcode(configForm.value.butlerQrcode))
}

View File

@ -75,6 +75,13 @@
<!-- ==================== 主内容区 ==================== -->
<!-- 固定导航栏 - 不随页面滚动 -->
<view class="fixed-navbar" :class="{ 'navbar-scrolled': isScrolled }" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="header-title">相宜相亲</view>
</view>
</view>
<!-- 整页滚动区域 - 支持下拉刷新和上拉加载更多 -->
<scroll-view
class="page-scroll"
@ -83,8 +90,9 @@
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleScrollToLower"
@scroll="handleScroll"
>
<!-- Banner 区域导航栏和搜索框浮在 Banner -->
<!-- Banner 区域搜索框浮在 Banner -->
<view class="banner-section">
<!-- Banner 轮播图 -->
<swiper
@ -110,22 +118,20 @@
<!-- banner 时的默认渐变背景 -->
<view v-else class="banner-placeholder"></view>
<!-- 浮层导航栏 + 搜索框 -->
<!-- 浮层搜索框 -->
<view class="banner-overlay">
<!-- 自定义导航栏 - 适配状态栏高度 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="header-title">相宜相亲</view>
</view>
<!-- 导航栏占位与固定导航栏等高 -->
<view class="navbar-placeholder" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content"></view>
</view>
<!-- 搜索框 - 点击跳转搜索页 -->
<view class="search-section">
<!-- 搜索框 - 暂时隐藏 -->
<!-- <view class="search-section">
<view class="search-bar" @click="handleSearchClick">
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索你心目中的TA</text>
</view>
</view>
</view> -->
</view>
</view>
@ -260,6 +266,9 @@ export default {
/** 下拉刷新状态 */
const isRefreshing = ref(false)
/** 页面是否已滚动(用于导航栏背景切换) */
const isScrolled = ref(false)
// ==================== ====================
@ -945,6 +954,13 @@ export default {
}
}
/**
* 页面滚动 - 控制导航栏背景
*/
const handleScroll = (e) => {
isScrolled.value = e.detail.scrollTop > 20
}
// ==================== ====================
onMounted(() => {
@ -1008,7 +1024,9 @@ export default {
handleGoMember,
handleScrollToLower,
handleRefresh,
handleCloseAuditingPopup
handleScroll,
handleCloseAuditingPopup,
isScrolled
}
},
@ -1098,9 +1116,17 @@ export default {
z-index: 10;
}
/* ==================== 自定义导航栏 ==================== */
/* ==================== 固定导航栏 ==================== */
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: transparent;
transition: background 0.3s ease;
.custom-navbar {
.navbar-content {
height: 44px;
display: flex;
@ -1116,6 +1142,17 @@ export default {
}
}
.navbar-scrolled {
background: #FF8A93 !important;
}
/* 导航栏占位(防止内容被固定导航栏遮挡) */
.navbar-placeholder {
.navbar-content {
height: 44px;
}
}
/* ==================== 搜索框 ==================== */
.search-section {
@ -1241,60 +1278,6 @@ export default {
right: 0;
bottom: 0;
z-index: 99;
.member-ad-bar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 110rpx;
padding: 0 24rpx;
.ad-content {
display: flex;
align-items: center;
flex: 1;
.ad-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.ad-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.ad-btn {
background: #fff;
color: #ff6b6b;
font-size: 24rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx;
margin-right: 16rpx;
font-weight: 500;
}
.ad-close {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 36rpx;
color: #666;
line-height: 1;
}
}
}
}
/* ==================== 订阅消息提醒条 ==================== */
@ -1306,4 +1289,59 @@ export default {
bottom: 0;
z-index: 98;
}
/* 广告条/提醒条公共样式 */
.member-ad-bar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 110rpx;
padding: 0 24rpx;
.ad-content {
display: flex;
align-items: center;
flex: 1;
.ad-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.ad-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.ad-btn {
background: #fff;
color: #ff6b6b;
font-size: 24rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx;
margin-right: 16rpx;
font-weight: 500;
}
.ad-close {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 36rpx;
color: #666;
line-height: 1;
}
}
}
</style>

View File

@ -160,7 +160,7 @@
<!-- 籍贯 -->
<view class="form-item">
<text class="form-label">籍贯</text>
<text class="form-label required">籍贯</text>
<picker
mode="region"
:value="[formData.homeProvince, formData.homeCity, formData.homeDistrict]"
@ -637,7 +637,7 @@
<!-- 手机号验证 -->
<view class="form-item">
<text class="form-label">手机号验证</text>
<text class="form-label required">手机号验证</text>
<button
v-if="!phoneVerified"
class="verify-btn"
@ -1358,6 +1358,10 @@ const validateStep = (step) => {
uni.showToast({ title: '请选择出生年份', icon: 'none' })
return false
}
if (!formData.homeProvince || !formData.homeCity) {
uni.showToast({ title: '请选择籍贯', icon: 'none' })
return false
}
return true
case 1: //
@ -1450,6 +1454,10 @@ const validateStep = (step) => {
uni.showToast({ title: '请输入微信号', icon: 'none' })
return false
}
if (!phoneVerified.value) {
uni.showToast({ title: '请先验证手机号', icon: 'none' })
return false
}
return true
default:
@ -1546,7 +1554,7 @@ const loadProfile = async () => {
formData.houseStatus = profile.houseStatus || 5
formData.carStatus = profile.carStatus || 2
formData.marriageStatus = profile.marriageStatus || 1
formData.expectMarryTime = profile.expectMarryTime || 0
formData.expectMarryTime = profile.expectMarryTime || 1
formData.introduction = profile.introduction || ''
formData.weChatNo = profile.weChatNo || ''

View File

@ -65,12 +65,20 @@
<!-- 可滚动内容区域 -->
<scroll-view class="content-scroll" scroll-y :style="{
top: (statusBarHeight + 44 + 100) + 'px',
height: 'calc(100vh - ' + (statusBarHeight + 44 + 100 + 120) + 'px)'
bottom: (currentStep === 1 && !canSkipPayment ? 240 : 140) + 'px'
}">
<!-- 步骤1: 支付页面 (Requirements 12.1) -->
<view v-if="currentStep === 1" class="step-payment">
<view class="payment-content">
<image class="realname-title-img" src="/static/ic_real_name.png" mode="widthFix" />
<!-- 实名认证Banner图 -->
<view class="realname-banner">
<image v-if="realNameBannerUrl" :src="realNameBannerUrl" mode="widthFix" class="banner-img" />
<view v-else class="banner-default">
<view class="banner-default-content">
<text class="banner-title">实名认证介绍</text>
</view>
</view>
</view>
<!-- 实名认证的好处 -->
<view class="benefits-section">
@ -95,12 +103,6 @@
</view>
</view>
</view>
<!-- 联系客服 -->
<view class="contact-service" @click="handleContactService">
<!-- <image class="service-avatar" src="/static/ic_service.png" mode="aspectFill" /> -->
<text class="service-text">联系客服</text>
</view>
</view>
</view>
@ -225,6 +227,9 @@
import {
getToken
} from '@/utils/storage.js'
import {
getFullImageUrl
} from '@/utils/image.js'
import Loading from '@/components/Loading/index.vue'
export default {
@ -249,6 +254,12 @@
// -
const verificationFee = computed(() => configStore.realNamePrice || 88)
// Banner
const realNameBannerUrl = computed(() => {
const url = configStore.realNameBanner
return url ? getFullImageUrl(url) : ''
})
//
const idCardNumber = ref('')
const realName = ref('')
@ -573,6 +584,7 @@
submitting,
currentStep,
verificationFee,
realNameBannerUrl,
idCardNumber,
realName,
isVerified,
@ -903,15 +915,39 @@
// 1:
.step-payment {
.payment-content {
padding: 40rpx 30rpx;
padding: 20rpx 30rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.realname-title-img {
width: 500rpx;
margin-bottom: 40rpx;
// Banner
.realname-banner {
width: 100%;
margin-bottom: 30rpx;
border-radius: 24rpx;
overflow: hidden;
.banner-img {
width: 100%;
display: block;
}
.banner-default {
width: 100%;
height: 320rpx;
background: linear-gradient(135deg, #FFB6C1 0%, #FFC0CB 50%, #FFE4E1 100%);
display: flex;
align-items: center;
justify-content: center;
.banner-default-content {
.banner-title {
font-size: 40rpx;
font-weight: 700;
color: #D4145A;
}
}
}
}
//
@ -924,7 +960,7 @@
.benefits-title {
font-size: 36rpx;
font-weight: 600;
color: #1890ff;
color: #333;
margin-bottom: 32rpx;
}
@ -950,114 +986,13 @@
color: #333;
.highlight {
color: #1890ff;
font-weight: 500;
color: #FF6A6A;
font-weight: 600;
}
}
}
}
}
//
.contact-service {
position: absolute;
right: 30rpx;
top: 480rpx;
display: flex;
flex-direction: column;
align-items: center;
.service-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-bottom: 8rpx;
}
.service-text {
font-size: 22rpx;
color: #666;
}
}
.payment-icon {
font-size: 100rpx;
margin-bottom: 24rpx;
}
.payment-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.payment-desc {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
}
.payment-benefits {
width: 100%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 40rpx;
.benefit-item {
display: flex;
align-items: center;
padding: 16rpx 0;
.benefit-icon {
width: 40rpx;
height: 40rpx;
background: #52c41a;
border-radius: 50%;
color: #fff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.benefit-text {
font-size: 28rpx;
color: #333;
}
}
}
.payment-price {
display: flex;
flex-direction: column;
align-items: center;
.price-label {
font-size: 26rpx;
color: #999;
margin-bottom: 12rpx;
}
.price-value {
display: flex;
align-items: baseline;
.symbol {
font-size: 32rpx;
color: #ff6b6b;
font-weight: 500;
}
.amount {
font-size: 72rpx;
color: #ff6b6b;
font-weight: 700;
}
}
}
}
}
@ -1389,7 +1324,7 @@
justify-content: center;
border-radius: 48rpx;
border: none;
background: linear-gradient(135deg, #ABCEFF 0%, #156EFF 100%);
background: linear-gradient(135deg, #FF9A9E 0%, #FF6B6B 100%);
&::after {
border: none;
@ -1406,7 +1341,7 @@
position: absolute;
right: 40rpx;
top: -16rpx;
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
background: linear-gradient(135deg, #4A90D9 0%, #2B6CB0 100%);
color: #fff;
font-size: 22rpx;
padding: 6rpx 16rpx;
@ -1421,7 +1356,7 @@
align-items: center;
justify-content: center;
border-radius: 48rpx;
border: 2rpx solid #0062FF;
border: 2rpx solid #FF6B6B;
background: #fff;
&::after {
@ -1431,7 +1366,7 @@
text {
font-size: 32rpx;
font-weight: 600;
color: #46A2FF;
color: #FF6B6B;
}
&[disabled] {
@ -1447,7 +1382,7 @@
justify-content: center;
border-radius: 48rpx;
border: none;
background: linear-gradient(135deg, #46A2FF 0%, #0062FF 100%);
background: linear-gradient(135deg, #FFABAC 0%, #FF5457 100%);
&::after {
border: none;

View File

@ -39,6 +39,7 @@ export const useConfigStore = defineStore('config', {
// 系统配置
defaultAvatar: getDefaultAvatar() || '/static/logo.png',
searchBanner: '',
realNameBanner: '',
butlerQrcode: '', // 管家指导二维码
memberEntryImage: '', // 会员入口图
@ -130,6 +131,7 @@ export const useConfigStore = defineStore('config', {
setDefaultAvatar(config.defaultAvatar)
}
this.searchBanner = config.searchBanner || ''
this.realNameBanner = config.realNameBanner || ''
this.butlerQrcode = config.butlerQrcode || ''
this.memberEntryImage = config.memberEntryImage || ''
@ -387,6 +389,7 @@ export const useConfigStore = defineStore('config', {
this.banners = []
this.kingKongs = []
this.searchBanner = ''
this.realNameBanner = ''
this.dailyPopup = null
this.memberAdConfig = null
this.serviceAccountPopup = null

View File

@ -175,6 +175,34 @@ public class AdminConfigController : ControllerBase
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
/// <summary>
/// 获取实名认证页Banner
/// </summary>
[HttpGet("realNameBanner")]
public async Task<ApiResponse<RealNameBannerResponse>> GetRealNameBanner()
{
var imageUrl = await _configService.GetRealNameBannerAsync();
return ApiResponse<RealNameBannerResponse>.Success(new RealNameBannerResponse
{
ImageUrl = imageUrl
});
}
/// <summary>
/// 设置实名认证页Banner
/// </summary>
[HttpPost("realNameBanner")]
public async Task<ApiResponse> SetRealNameBanner([FromBody] SetRealNameBannerRequest request)
{
if (string.IsNullOrWhiteSpace(request.ImageUrl))
{
return ApiResponse.Error(40001, "图片URL不能为空");
}
var result = await _configService.SetRealNameBannerAsync(request.ImageUrl);
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
}
/// <summary>
/// 获取管家二维码
/// </summary>
@ -396,6 +424,28 @@ public class SetSearchBannerRequest
public string ImageUrl { get; set; } = string.Empty;
}
/// <summary>
/// 实名认证页Banner响应
/// </summary>
public class RealNameBannerResponse
{
/// <summary>
/// 图片URL
/// </summary>
public string? ImageUrl { get; set; }
}
/// <summary>
/// 设置实名认证页Banner请求
/// </summary>
public class SetRealNameBannerRequest
{
/// <summary>
/// 图片URL
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
}
/// <summary>
/// 管家二维码响应
/// </summary>

View File

@ -42,10 +42,10 @@
"GhId": "gh_f57692c34c0e",
"AppId": "wxa2f42b01be34b37b",
"AppSecret": "b8cc3abd81a7cbf32ded28f92ba61627",
"UnlockTemplateId": "dQdK2i7ZDkDGQ2Knifv82rDx9HCzR1aE71YmR8JjwBc",
"FavoriteTemplateId": "dQdK2i7ZDkDGQ2Knifv82rDx9HCzR1aE71YmR8JjwBc",
"MessageTemplateId": "dQdK2i7ZDkDGQ2Knifv82rDx9HCzR1aE71YmR8JjwBc",
"DailyRecommendTemplateId": "dQdK2i7ZDkDGQ2Knifv82rDx9HCzR1aE71YmR8JjwBc",
"UnlockTemplateId": "1WwIIY4NoPWE972HfSgjmqcjmq59ihFfrUsuqlFGMzk",
"FavoriteTemplateId": "1WwIIY4NoPWE972HfSgjmqcjmq59ihFfrUsuqlFGMzk",
"MessageTemplateId": "1WwIIY4NoPWE972HfSgjmqcjmq59ihFfrUsuqlFGMzk",
"DailyRecommendTemplateId": "1WwIIY4NoPWE972HfSgjmqcjmq59ihFfrUsuqlFGMzk",
"FollowArticleUrl": ""
},
"SubscribeMessage": {

View File

@ -91,6 +91,11 @@ public class AppConfigResponse
/// </summary>
public string? SearchBanner { get; set; }
/// <summary>
/// 实名认证页Banner URL
/// </summary>
public string? RealNameBanner { get; set; }
/// <summary>
/// 管家指导二维码URL
/// </summary>

View File

@ -72,6 +72,16 @@ public interface ISystemConfigService
/// </summary>
Task<bool> SetSearchBannerAsync(string imageUrl);
/// <summary>
/// 获取实名认证页Banner图URL
/// </summary>
Task<string?> GetRealNameBannerAsync();
/// <summary>
/// 设置实名认证页Banner图URL
/// </summary>
Task<bool> SetRealNameBannerAsync(string imageUrl);
/// <summary>
/// 获取管家指导二维码URL
/// </summary>

View File

@ -391,7 +391,7 @@ public class AdminUserService : IAdminUserService
IsRealName = random.Next(2) == 1,
IsMember = random.Next(3) == 0, // 1/3概率是会员
MemberLevel = 0,
ContactCount = 2,
ContactCount = 0,
CreateTime = DateTime.Now.AddDays(-random.Next(30)),
UpdateTime = DateTime.Now,
LastLoginTime = DateTime.Now.AddHours(-random.Next(72))

View File

@ -79,7 +79,7 @@ public class AuthService : IAuthService
XiangQinNo = xiangQinNo,
Avatar = defaultAvatar,
Status = 1,
ContactCount = 2,
ContactCount = 0,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};

View File

@ -47,6 +47,7 @@ public class ConfigService : IConfigService
var kingKongs = await GetKingKongsAsync();
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync();
var searchBanner = await _systemConfigService.GetSearchBannerAsync();
var realNameBanner = await _systemConfigService.GetRealNameBannerAsync();
var butlerQrcode = await _systemConfigService.GetButlerQrcodeAsync();
var memberIcons = await _systemConfigService.GetMemberIconsAsync();
var dailyPopup = await GetPopupConfigAsync(1); // 每日弹窗
@ -62,6 +63,7 @@ public class ConfigService : IConfigService
KingKongs = kingKongs,
DefaultAvatar = defaultAvatar,
SearchBanner = searchBanner,
RealNameBanner = realNameBanner,
ButlerQrcode = butlerQrcode,
MemberIcons = memberIcons,
DailyPopup = dailyPopup,

View File

@ -255,10 +255,12 @@ public class RealNameService : IRealNameService
// 更新用户实名状态
user.IsRealName = true;
// 权益1实名认证赠送1次联系次数
user.ContactCount += 1;
user.UpdateTime = DateTime.Now;
await _userRepository.UpdateAsync(user);
_logger.LogInformation("实名认证成功: UserId={UserId}", userId);
_logger.LogInformation("实名认证成功: UserId={UserId}, 赠送1次联系次数, 当前={ContactCount}", userId, user.ContactCount);
return new RealNameSubmitResponse
{
@ -519,10 +521,12 @@ public class RealNameService : IRealNameService
// 更新用户实名状态
user.IsRealName = true;
// 权益1实名认证赠送1次联系次数
user.ContactCount += 1;
user.UpdateTime = DateTime.Now;
await _userRepository.UpdateAsync(user);
_logger.LogInformation("实名认证成功: UserId={UserId}", userId);
_logger.LogInformation("实名认证成功: UserId={UserId}, 赠送1次联系次数, 当前={ContactCount}", userId, user.ContactCount);
return new RealNameVerifyResponse
{

View File

@ -25,7 +25,7 @@ public class RecommendService : IRecommendService
/// <summary>
/// 普通用户推荐数量
/// </summary>
public const int NormalUserRecommendCount = 10;
public const int NormalUserRecommendCount = 4;
/// <summary>
/// 不限时会员推荐数量
@ -539,8 +539,114 @@ public class RecommendService : IRecommendService
return await GenerateDailyRecommendForUserAsync(userId);
}
/// <summary>
/// 计算候选用户满足择偶要求的条件数量(静态方法,用于测试)
/// </summary>
public static (int matched, int total) CalculateMatchedConditions(UserRequirement? requirement, UserProfile targetProfile)
{
if (requirement == null) return (0, 0);
var matched = 0;
var total = 0;
// 年龄条件
if (requirement.AgeMin > 0 || requirement.AgeMax > 0)
{
total++;
var targetAge = DateTime.Now.Year - targetProfile.BirthYear;
if ((requirement.AgeMin <= 0 || targetAge >= requirement.AgeMin) &&
(requirement.AgeMax <= 0 || targetAge <= requirement.AgeMax))
{
matched++;
}
}
// 身高条件
if (requirement.HeightMin.HasValue || requirement.HeightMax.HasValue)
{
total++;
var heightOk = true;
if (requirement.HeightMin.HasValue && targetProfile.Height < requirement.HeightMin.Value) heightOk = false;
if (requirement.HeightMax.HasValue && targetProfile.Height > requirement.HeightMax.Value) heightOk = false;
if (heightOk) matched++;
}
// 学历条件
if (!string.IsNullOrEmpty(requirement.Education))
{
var eduList = ParseJsonArray(requirement.Education);
if (eduList.Count > 0)
{
total++;
if (eduList.Contains(targetProfile.Education)) matched++;
}
}
// 城市条件
if (!string.IsNullOrEmpty(requirement.City1City) || !string.IsNullOrEmpty(requirement.City2City))
{
total++;
if ((!string.IsNullOrEmpty(requirement.City1City) && targetProfile.WorkCity == requirement.City1City) ||
(!string.IsNullOrEmpty(requirement.City2City) && targetProfile.WorkCity == requirement.City2City))
{
matched++;
}
}
// 收入条件
if (requirement.MonthlyIncomeMin.HasValue || requirement.MonthlyIncomeMax.HasValue)
{
total++;
var incomeOk = true;
if (requirement.MonthlyIncomeMin.HasValue && targetProfile.MonthlyIncome < requirement.MonthlyIncomeMin.Value) incomeOk = false;
if (requirement.MonthlyIncomeMax.HasValue && targetProfile.MonthlyIncome > requirement.MonthlyIncomeMax.Value) incomeOk = false;
if (incomeOk) matched++;
}
// 房产条件
if (!string.IsNullOrEmpty(requirement.HouseStatus))
{
var houseList = ParseJsonArray(requirement.HouseStatus);
if (houseList.Count > 0)
{
total++;
if (houseList.Contains(targetProfile.HouseStatus)) matched++;
}
}
// 车辆条件
if (!string.IsNullOrEmpty(requirement.CarStatus))
{
var carList = ParseJsonArray(requirement.CarStatus);
if (carList.Count > 0)
{
total++;
if (carList.Contains(targetProfile.CarStatus)) matched++;
}
}
// 婚姻状态条件
if (!string.IsNullOrEmpty(requirement.MarriageStatus))
{
var marriageList = ParseJsonArray(requirement.MarriageStatus);
if (marriageList.Count > 0)
{
total++;
if (marriageList.Contains(targetProfile.MarriageStatus)) matched++;
}
}
return (matched, total);
}
/// <summary>
/// 获取候选用户列表
/// 推荐优先级:
/// 1. 全部满足择偶要求的候选人
/// 2. 逐一减少满足条件数量
/// 3. 同城优先,已实名用户优先
/// 4. 家乡城市较近,已实名用户优先
/// 原则上不推荐3天内推荐过的用户
/// </summary>
private async Task<List<CandidateUser>> GetCandidateUsersAsync(
long userId,
@ -576,6 +682,9 @@ public class RecommendService : IRecommendService
? CalculateMatchScoreStatic(userRequirement, profile)
: 50;
// 计算满足择偶要求的条件数量
var (matchedCount, totalCount) = CalculateMatchedConditions(userRequirement, profile);
// 检查是否在去重期内
var isRecentlyRecommended = await IsRecommendedInDaysAsync(userId, user.Id, DeduplicationDays);
@ -583,26 +692,42 @@ public class RecommendService : IRecommendService
{
UserId = user.Id,
MatchScore = matchScore,
MatchedConditionCount = matchedCount,
TotalConditionCount = totalCount,
IsNewUser = user.CreateTime > DateTime.Now.AddMinutes(-NewUserPriorityMinutes),
IsMember = user.IsMember,
IsRealName = user.IsRealName,
IsRecentlyRecommended = isRecentlyRecommended,
WorkCity = profile.WorkCity
WorkCity = profile.WorkCity,
WorkProvince = profile.WorkProvince,
HomeCity = profile.HomeCity,
HomeProvince = profile.HomeProvince
});
}
// 排序逻辑:
// 1. 新用户优先30分钟内注册推荐给会员
// 2. 未在去重期内的用户优先
// 3. 按匹配度排序
// 4. 同城优先
// 排序逻辑(新推荐优先级):
// 1. 未在去重期内的用户优先
// 2. 满足择偶要求的条件数量多的优先(全部满足 > 逐一减少)
// 3. 同城(工作城市相同)优先,同城中已实名用户优先
// 4. 同省优先
// 5. 家乡城市相同优先,其中已实名用户优先
// 6. 家乡省份相同优先
// 7. 会员优先看新用户
var currentUser = await _userRepository.GetByIdAsync(userId);
var isCurrentUserMember = currentUser?.IsMember ?? false;
var sortedCandidates = candidates
.OrderByDescending(c => isCurrentUserMember && c.IsNewUser ? 1 : 0) // 会员优先看新用户
.ThenBy(c => c.IsRecentlyRecommended ? 1 : 0) // 未推荐过的优先
.ThenByDescending(c => c.MatchScore) // 匹配度高的优先
.ThenByDescending(c => c.WorkCity == userProfile.WorkCity ? 1 : 0) // 同城优先
.OrderBy(c => c.IsRecentlyRecommended ? 1 : 0) // 未推荐过的优先
.ThenByDescending(c => c.MatchedConditionCount) // 满足条件数量多的优先
.ThenByDescending(c => c.WorkCity == userProfile.WorkCity ? 1 : 0) // 同城(工作城市)优先
.ThenByDescending(c => c.WorkCity == userProfile.WorkCity && c.IsRealName ? 1 : 0) // 同城中已实名优先
.ThenByDescending(c => c.WorkProvince == userProfile.WorkProvince ? 1 : 0) // 同省优先
.ThenByDescending(c => c.HomeCity == userProfile.HomeCity ? 1 : 0) // 家乡城市相同优先
.ThenByDescending(c => c.HomeCity == userProfile.HomeCity && c.IsRealName ? 1 : 0) // 家乡城市相同中已实名优先
.ThenByDescending(c => c.HomeProvince == userProfile.HomeProvince ? 1 : 0) // 家乡省份相同优先
.ThenByDescending(c => c.IsRealName ? 1 : 0) // 已实名用户优先
.ThenByDescending(c => isCurrentUserMember && c.IsNewUser ? 1 : 0) // 会员优先看新用户
.ThenByDescending(c => c.MatchScore) // 匹配度高的优先
.ToList();
// 如果候选用户不足,允许重复推荐
@ -668,7 +793,19 @@ public class RecommendService : IRecommendService
public int MatchScore { get; set; }
public bool IsNewUser { get; set; }
public bool IsMember { get; set; }
public bool IsRealName { get; set; }
public bool IsRecentlyRecommended { get; set; }
public string? WorkCity { get; set; }
public string? WorkProvince { get; set; }
public string? HomeCity { get; set; }
public string? HomeProvince { get; set; }
/// <summary>
/// 满足择偶要求的条件数量(用于逐一减少排序)
/// </summary>
public int MatchedConditionCount { get; set; }
/// <summary>
/// 择偶要求总条件数
/// </summary>
public int TotalConditionCount { get; set; }
}
}

View File

@ -38,6 +38,11 @@ public class SystemConfigService : ISystemConfigService
/// </summary>
public const string SearchBannerKey = "search_banner";
/// <summary>
/// 实名认证页Banner配置键
/// </summary>
public const string RealNameBannerKey = "realname_banner";
/// <summary>
/// 管家二维码配置键
/// </summary>
@ -211,6 +216,18 @@ public class SystemConfigService : ISystemConfigService
return await SetConfigValueAsync(SearchBannerKey, imageUrl, "搜索页Banner图URL");
}
/// <inheritdoc />
public async Task<string?> GetRealNameBannerAsync()
{
return await GetConfigValueAsync(RealNameBannerKey);
}
/// <inheritdoc />
public async Task<bool> SetRealNameBannerAsync(string imageUrl)
{
return await SetConfigValueAsync(RealNameBannerKey, imageUrl, "实名认证页Banner图URL");
}
/// <inheritdoc />
public async Task<string?> GetButlerQrcodeAsync()
{

View File

@ -97,9 +97,9 @@ public class User : SoftDeleteEntity
public DateTime? MemberExpireTime { get; set; }
/// <summary>
/// 剩余联系次数,默认2
/// 剩余联系次数,默认0
/// </summary>
public int ContactCount { get; set; } = 2;
public int ContactCount { get; set; } = 0;
/// <summary>
/// 最后登录时间

View File

@ -20,8 +20,8 @@ public class RecommendCountPropertyTests
[Property(MaxTest = 100)]
public Property RecommendCount_ShouldMatchMemberLevel()
{
// 生成有效的会员等级0-3
var memberLevelArb = Gen.Choose(0, 3);
// 生成有效的会员等级0-4
var memberLevelArb = Gen.Choose(0, 4);
return Prop.ForAll(
memberLevelArb.ToArbitrary(),
@ -34,9 +34,9 @@ public class RecommendCountPropertyTests
// Assert
return memberLevel switch
{
// 普通用户:恰好10
// 普通用户:恰好4
0 => actualCount == RecommendService.NormalUserRecommendCount &&
minCount == 10 && maxCount == 10,
minCount == 4 && maxCount == 4,
// 不限时会员恰好24人
1 => actualCount == RecommendService.UnlimitedMemberRecommendCount &&
@ -47,8 +47,13 @@ public class RecommendCountPropertyTests
actualCount <= RecommendService.SincereMemberMaxRecommendCount &&
minCount == 24 && maxCount == 29,
// 家庭版会员恰好24人
3 => actualCount == RecommendService.FamilyMemberRecommendCount &&
// 家庭版会员24-29人
3 => actualCount >= RecommendService.SincereMemberMinRecommendCount &&
actualCount <= RecommendService.SincereMemberMaxRecommendCount &&
minCount == 24 && maxCount == 29,
// 限时会员恰好24人
4 => actualCount == RecommendService.UnlimitedMemberRecommendCount &&
minCount == 24 && maxCount == 24,
_ => false
@ -57,7 +62,7 @@ public class RecommendCountPropertyTests
}
/// <summary>
/// 普通用户推荐数量应为10
/// 普通用户推荐数量应为4
/// </summary>
[Property(MaxTest = 100)]
public Property NormalUser_ShouldGet10Recommendations()
@ -67,7 +72,7 @@ public class RecommendCountPropertyTests
_ =>
{
var count = RecommendService.GetRecommendCountByMemberLevelStatic(0);
return count == 10;
return count == 4;
});
}
@ -102,7 +107,7 @@ public class RecommendCountPropertyTests
}
/// <summary>
/// 家庭版会员推荐数量应为24
/// 家庭版会员推荐数量应在24-29之间
/// </summary>
[Property(MaxTest = 100)]
public Property FamilyMember_ShouldGet24Recommendations()
@ -112,7 +117,7 @@ public class RecommendCountPropertyTests
_ =>
{
var count = RecommendService.GetRecommendCountByMemberLevelStatic(3);
return count == 24;
return count >= 24 && count <= 29;
});
}
@ -122,10 +127,10 @@ public class RecommendCountPropertyTests
[Property(MaxTest = 100)]
public Property InvalidMemberLevel_ShouldReturnNormalUserCount()
{
// 生成无效的会员等级(负数或大于3
// 生成无效的会员等级(负数或大于4
var invalidLevelArb = Gen.OneOf(
Gen.Choose(-100, -1),
Gen.Choose(4, 100)
Gen.Choose(5, 100)
);
return Prop.ForAll(
@ -143,7 +148,7 @@ public class RecommendCountPropertyTests
[Property(MaxTest = 100)]
public Property MemberRecommendCount_ShouldBeGreaterOrEqualToNormalUser()
{
var memberLevelArb = Gen.Choose(1, 3); // 会员等级1-3
var memberLevelArb = Gen.Choose(1, 4); // 会员等级1-4
return Prop.ForAll(
memberLevelArb.ToArbitrary(),
@ -161,7 +166,7 @@ public class RecommendCountPropertyTests
[Fact]
public void Constants_ShouldHaveCorrectValues()
{
Assert.Equal(10, RecommendService.NormalUserRecommendCount);
Assert.Equal(4, RecommendService.NormalUserRecommendCount);
Assert.Equal(24, RecommendService.UnlimitedMemberRecommendCount);
Assert.Equal(24, RecommendService.SincereMemberMinRecommendCount);
Assert.Equal(29, RecommendService.SincereMemberMaxRecommendCount);