This commit is contained in:
zpc 2025-12-08 00:12:17 +08:00
parent 633d47fa12
commit 4a80fc27ca
9 changed files with 447 additions and 227 deletions

View File

@ -151,5 +151,12 @@ namespace CoreCms.Net.Model.Entities
/// </summary> /// </summary>
[Display(Name = "订单id")] [Display(Name = "订单id")]
public string paymentId { get; set; } public string paymentId { get; set; }
/// <summary>
/// 签到时间
/// </summary>
[Display(Name = "签到时间")]
public System.DateTime? check_reservation { get; set; }
} }
} }

View File

@ -164,9 +164,6 @@ namespace CoreCms.Net.Model.Entities
[Display(Name = "是否禁烟0=不限制1=禁烟2=不禁烟")] [Display(Name = "是否禁烟0=不限制1=禁烟2=不禁烟")]
[Required(ErrorMessage = "请输入{0}")] [Required(ErrorMessage = "请输入{0}")]
public System.Int32 is_smoking { get; set; } public System.Int32 is_smoking { get; set; }
@ -174,11 +171,7 @@ namespace CoreCms.Net.Model.Entities
/// 性别限制0=不限1=男2=女 /// 性别限制0=不限1=男2=女
/// </summary> /// </summary>
[Display(Name = "性别限制0=不限1=男2=女")] [Display(Name = "性别限制0=不限1=男2=女")]
[Required(ErrorMessage = "请输入{0}")] [Required(ErrorMessage = "请输入{0}")]
public System.Int32 gender_limit { get; set; } public System.Int32 gender_limit { get; set; }
@ -186,11 +179,6 @@ namespace CoreCms.Net.Model.Entities
/// 最低信誉分 /// 最低信誉分
/// </summary> /// </summary>
[Display(Name = "最低信誉分")] [Display(Name = "最低信誉分")]
public System.Decimal? credit_limit { get; set; } public System.Decimal? credit_limit { get; set; }

View File

@ -1,4 +1,4 @@
/*********************************************************************** /***********************************************************************
* Project: CoreCms * Project: CoreCms
* ProjectName: * ProjectName:
* Web: https://www.corecms.net * Web: https://www.corecms.net
@ -133,6 +133,12 @@ namespace CoreCms.Net.Repository
isDelete = p.isDelete, isDelete = p.isDelete,
type = (int)sWeChatInfo.type, type = (int)sWeChatInfo.type,
parentNickName = sParentUser.nickName, parentNickName = sParentUser.nickName,
dove_count=p.dove_count,
credit_score=p.credit_score,
pending_earnings=p.pending_earnings,
play_level=p.play_level,
skills_level=p.skills_level,
withdrawn_earnings=p.withdrawn_earnings,
childNum = SqlFunc.Subqueryable<CoreCmsUser>().Where(o => o.parentId == p.id).Count() childNum = SqlFunc.Subqueryable<CoreCmsUser>().Where(o => o.parentId == p.id).Count()
}) })
.MergeTable().With(SqlWith.Null) .MergeTable().With(SqlWith.Null)

View File

@ -33,7 +33,10 @@
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label"><span class="x-red">*</span>房费</label> <label class="layui-form-label"><span class="x-red">*</span>房费</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="number" name="room_fee" id="room_fee" placeholder="请输入房费" class="layui-input" lay-verify="required|number" lay-reqtext="请输入房费" step="0.01" value="0"> <div style="display: flex; align-items: center; gap: 10px;">
<input type="number" name="room_fee" id="room_fee" placeholder="请输入房费" class="layui-input" lay-verify="required|number" lay-reqtext="请输入房费" step="0.01" value="0" style="flex: 1;">
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm" id="calcBtn">计算10%佣金</button>
</div>
</div> </div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
@ -57,12 +60,9 @@
<textarea name="remark" placeholder="请输入备注" class="layui-textarea"></textarea> <textarea name="remark" placeholder="请输入备注" class="layui-textarea"></textarea>
</div> </div>
</div> </div>
<div class="layui-form-item"> <!-- 隐藏的提交按钮,供弹窗底部按钮触发 -->
<div class="layui-input-block"> <div style="display: none;">
<button type="button" class="layui-btn" lay-submit lay-filter="LAY-app-SQEarnings-add-submit">提交</button> <button type="button" class="layui-btn" lay-submit lay-filter="LAY-app-SQEarnings-add-submit" id="LAY-app-SQEarnings-add-submit">提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
<button type="button" class="layui-btn layui-btn-primary" id="calcBtn">计算10%佣金</button>
</div>
</div> </div>
</form> </form>
@ -133,6 +133,16 @@
var form = layui.form; var form = layui.form;
var coreHelper = layui.coreHelper; var coreHelper = layui.coreHelper;
// 检查是否有预选的用户信息(从用户列表页面传入)
if (d && d.data && d.data.preSelectedUser) {
var preUser = d.data.preSelectedUser;
$('#user_id').val(preUser.id);
$('#user_display').val(preUser.displayName);
// 默认设置为佣金类型
$('select[name="type"]').val('1');
form.render('select');
}
// 加载房间列表 // 加载房间列表
coreHelper.Post('Api/SQEarnings/GetRoomList', {}, function (res) { coreHelper.Post('Api/SQEarnings/GetRoomList', {}, function (res) {
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {

View File

@ -82,13 +82,15 @@
<script> <script>
layui.data.done = function (d) { layui.data.done = function (d) {
var $ = layui.$; layui.use(['admin', 'table', 'dropdown', 'form', 'view', 'coreHelper'], function () {
var admin = layui.admin; var $ = layui.$;
var setter = layui.setter; var admin = layui.admin;
var table = layui.table; var setter = layui.setter;
var dropdown = layui.dropdown; var table = layui.table;
var form = layui.form; var dropdown = layui.dropdown;
var view = layui.view; var form = layui.form;
var view = layui.view;
var coreHelper = layui.coreHelper;
table.render({ table.render({
elem: '#LAY-app-SQEarnings-tableBox', elem: '#LAY-app-SQEarnings-tableBox',
@ -152,17 +154,12 @@
} }
layer.confirm('确定要删除选中的数据吗?', function (index) { layer.confirm('确定要删除选中的数据吗?', function (index) {
var ids = data.map(function (item) { return item.id; }); var ids = data.map(function (item) { return item.id; });
admin.req({ coreHelper.Post('Api/SQEarnings/DoBatchDelete', { id: ids }, function (res) {
url: layui.setter.apiUrl + 'Api/SQEarnings/DoBatchDelete', if (res.code === 0) {
data: JSON.stringify({ id: ids }), layer.msg('删除成功');
type: 'post', table.reload('LAY-app-SQEarnings-tableBox');
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('删除成功');
table.reload('LAY-app-SQEarnings-tableBox');
} else {
layer.msg(res.msg);
}
} }
}); });
layer.close(index); layer.close(index);
@ -199,20 +196,16 @@
table.on('tool(LAY-app-SQEarnings-tableBox)', function (obj) { table.on('tool(LAY-app-SQEarnings-tableBox)', function (obj) {
var data = obj.data; var data = obj.data;
if (obj.event === 'del') { if (obj.event === 'del') {
admin.req({ coreHelper.Post('Api/SQEarnings/DoDelete', { id: data.id }, function (res) {
url: layui.setter.apiUrl + 'Api/SQEarnings/DoDelete', if (res.code === 0) {
data: JSON.stringify({ id: data.id }), layer.msg('删除成功');
type: 'post', obj.del();
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('删除成功');
obj.del();
} else {
layer.msg(res.msg);
}
} }
}); });
} }
}); });
});
}; };
</script> </script>

View File

@ -88,12 +88,14 @@
<script> <script>
layui.data.done = function (d) { layui.data.done = function (d) {
var $ = layui.$; layui.use(['admin', 'table', 'dropdown', 'form', 'coreHelper'], function () {
var admin = layui.admin; var $ = layui.$;
var setter = layui.setter; var admin = layui.admin;
var table = layui.table; var setter = layui.setter;
var dropdown = layui.dropdown; var table = layui.table;
var form = layui.form; var dropdown = layui.dropdown;
var form = layui.form;
var coreHelper = layui.coreHelper;
table.render({ table.render({
elem: '#LAY-app-SQWithdraw-tableBox', elem: '#LAY-app-SQWithdraw-tableBox',
@ -139,17 +141,12 @@
} }
layer.confirm('确定要删除选中的数据吗?', function (index) { layer.confirm('确定要删除选中的数据吗?', function (index) {
var ids = data.map(function (item) { return item.id; }); var ids = data.map(function (item) { return item.id; });
admin.req({ coreHelper.Post('Api/SQWithdraw/DoBatchDelete', { id: ids }, function (res) {
url: layui.setter.apiUrl + 'Api/SQWithdraw/DoBatchDelete', if (res.code === 0) {
data: JSON.stringify({ id: ids }), layer.msg('删除成功');
type: 'post', table.reload('LAY-app-SQWithdraw-tableBox');
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('删除成功');
table.reload('LAY-app-SQWithdraw-tableBox');
} else {
layer.msg(res.msg);
}
} }
}); });
layer.close(index); layer.close(index);
@ -162,32 +159,22 @@
table.on('tool(LAY-app-SQWithdraw-tableBox)', function (obj) { table.on('tool(LAY-app-SQWithdraw-tableBox)', function (obj) {
var data = obj.data; var data = obj.data;
if (obj.event === 'del') { if (obj.event === 'del') {
admin.req({ coreHelper.Post('Api/SQWithdraw/DoDelete', { id: data.id }, function (res) {
url: layui.setter.apiUrl + 'Api/SQWithdraw/DoDelete', if (res.code === 0) {
data: JSON.stringify({ id: data.id }), layer.msg('删除成功');
type: 'post', obj.del();
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('删除成功');
obj.del();
} else {
layer.msg(res.msg);
}
} }
}); });
} else if (obj.event === 'approve') { } else if (obj.event === 'approve') {
layer.confirm('确定已线下打款给该用户?', function (index) { layer.confirm('确定已线下打款给该用户?', function (index) {
admin.req({ coreHelper.Post('Api/SQWithdraw/DoApprove', { id: data.id }, function (res) {
url: layui.setter.apiUrl + 'Api/SQWithdraw/DoApprove', if (res.code === 0) {
data: JSON.stringify({ id: data.id }), layer.msg('操作成功');
type: 'post', table.reload('LAY-app-SQWithdraw-tableBox');
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('操作成功');
table.reload('LAY-app-SQWithdraw-tableBox');
} else {
layer.msg(res.msg);
}
} }
}); });
layer.close(index); layer.close(index);
@ -201,22 +188,18 @@
if (!value) { if (!value) {
return layer.msg('请输入拒绝原因'); return layer.msg('请输入拒绝原因');
} }
admin.req({ coreHelper.Post('Api/SQWithdraw/DoReject', { id: data.id, remark: value }, function (res) {
url: layui.setter.apiUrl + 'Api/SQWithdraw/DoReject', if (res.code === 0) {
data: JSON.stringify({ id: data.id, remark: value }), layer.msg('操作成功');
type: 'post', table.reload('LAY-app-SQWithdraw-tableBox');
done: function (res) { } else {
if (res.code === 0) { layer.msg(res.msg);
layer.msg('操作成功');
table.reload('LAY-app-SQWithdraw-tableBox');
} else {
layer.msg(res.msg);
}
} }
}); });
layer.close(promptIndex); layer.close(promptIndex);
}); });
} }
}); });
});
}; };
</script> </script>

View File

@ -106,8 +106,7 @@
<script type="text/html" id="LAY-app-CoreCmsUser-tableBox-bar"> <script type="text/html" id="LAY-app-CoreCmsUser-tableBox-bar">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a> <a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-xs" lay-event="editBalance">修改余额</a> <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="addCommission">添加佣金</a>
<a class="layui-btn layui-btn-xs" lay-event="editPoint">修改积分</a>
</script> </script>
<script> <script>
@ -205,15 +204,41 @@
}, },
{ field: 'nickName', title: '昵称', sort: false }, { field: 'nickName', title: '昵称', sort: false },
{ {
field: 'balance', title: '余额', sort: false, width: 70, templet: function (data) { field: 'totalEarnings', title: '全部收益', sort: false, width: 90, templet: function (data) {
var html = '<a class="link-hot option-show balance" data-id="' + data.id + '">' + data.balance + '</a>'; var total = (parseFloat(data.pending_earnings) || 0) + (parseFloat(data.withdrawn_earnings) || 0);
return html; return '<span class="layui-badge layui-badge-orange">' + total.toFixed(2) + '</span>';
} }
}, },
{ {
field: 'point', title: '积分', sort: false, width: 70, templet: function (data) { field: 'pending_earnings', title: '待提现', sort: false, width: 90, templet: function (data) {
var html = '<a class="link-hot option-show point" data-id="' + data.id + '">' + data.point + '</a>'; return '<span class="layui-badge layui-badge-blue">' + (parseFloat(data.pending_earnings) || 0).toFixed(2) + '</span>';
return html; }
},
{
field: 'withdrawn_earnings', title: '已提现', sort: false, width: 90, templet: function (data) {
return '<span class="layui-badge layui-badge-green">' + (parseFloat(data.withdrawn_earnings) || 0).toFixed(2) + '</span>';
}
},
{
field: 'play_level', title: '牌品', sort: false, width: 80, templet: function (data) {
return (parseFloat(data.play_level) || 0).toFixed(1);
}
},
{
field: 'skills_level', title: '牌技', sort: false, width: 80, templet: function (data) {
return (parseFloat(data.skills_level) || 0).toFixed(1);
}
},
{
field: 'credit_score', title: '信誉分', sort: false, width: 80, templet: function (data) {
return '<span class="layui-badge layui-badge-cyan">' + (parseFloat(data.credit_score) || 0).toFixed(1) + '</span>';
}
},
{
field: 'dove_count', title: '鸽子次数', sort: false, width: 90, templet: function (data) {
var count = parseInt(data.dove_count) || 0;
var badgeClass = count === 0 ? 'layui-badge-green' : (count <= 2 ? 'layui-badge-orange' : 'layui-badge');
return '<span class="layui-badge ' + badgeClass + '">' + count + '</span>';
} }
}, },
{ {
@ -254,7 +279,7 @@
} }
}, },
//{ field: 'isDelete', title: '删除标志', width: 105, templet: '#switch_isDelete', sort: false, unresize: true }, //{ field: 'isDelete', title: '删除标志', width: 105, templet: '#switch_isDelete', sort: false, unresize: true },
{ width: 202, align: 'center', title: '操作', fixed: 'right', toolbar: '#LAY-app-CoreCmsUser-tableBox-bar' } { width: 140, align: 'center', title: '操作', fixed: 'right', toolbar: '#LAY-app-CoreCmsUser-tableBox-bar' }
] ]
] ]
}); });
@ -311,10 +336,8 @@
doDelete(obj); doDelete(obj);
} else if (obj.event === 'edit') { } else if (obj.event === 'edit') {
doEdit(obj) doEdit(obj)
} else if (obj.event === 'editBalance') { } else if (obj.event === 'addCommission') {
doEditBalance(obj); doAddCommission(obj);
} else if (obj.event === 'editPoint') {
doEditPoint(obj)
} }
}); });
//执行创建操作 //执行创建操作
@ -400,81 +423,71 @@
}); });
} }
//执行编辑余额 //执行添加佣金
function doEditBalance(obj) { function doAddCommission(obj) {
coreHelper.Post("Api/CoreCmsUser/GetEditBalance", { id: obj.data.id }, function (e) { coreHelper.Post("Api/SQEarnings/GetCreate", null, function (e) {
if (e.code === 0) { if (e.code === 0) {
admin.popup({ admin.popup({
shadeClose: false, shadeClose: false,
title: '修改余额', title: '添加佣金 - ' + (obj.data.nickName || '用户ID:' + obj.data.id),
area: ['500px', '300px'], area: ['700px', '600px'],
id: 'LAY-popup-CoreCmsUser-EditBalance', id: 'LAY-popup-CoreCmsUser-AddCommission',
success: function (layero, index) { success: function (layero, index) {
view(this.id).render('user/userInfo/editBalance', { data: e.data }).done(function () { // 传递用户信息到页面
//监听提交 var pageData = e.data || {};
form.on('submit(LAY-app-CoreCmsUser-editBalanceForm-submit)', pageData.preSelectedUser = {
function (data) { id: obj.data.id,
var field = data.field; //获取提交的字段 nickName: obj.data.nickName || '',
field.data = parseFloat(field.data).toFixed(2); mobile: obj.data.mobile || '',
displayName: (obj.data.nickName || '') + 'ID: ' + obj.data.id + ''
};
//提交 Ajax 成功后,关闭当前弹层并重载表格 view(this.id).render('user/sqearnings/add', { data: pageData }).done(function () {
coreHelper.Post("Api/CoreCmsUser/DoEditBalance", field, function (e) { // 预填充用户信息和设置收益类型add.html 中已处理,这里确保设置)
console.log(e) if (pageData.preSelectedUser) {
if (e.code === 0) { $('#user_id').val(pageData.preSelectedUser.id);
layui.table.reloadData('LAY-app-CoreCmsUser-tableBox'); //重载表格 $('#user_display').val(pageData.preSelectedUser.displayName);
layer.close(index); //再执行关闭 }
layer.msg(e.msg); $('select[name="type"]').val('1');
} else { form.render('select');
layer.msg(e.msg);
} // 覆盖提交监听,确保 type=1 并刷新用户列表
}); form.on('submit(LAY-app-SQEarnings-add-submit)', function (data) {
var field = data.field;
var formData = {
user_id: parseInt(field.user_id),
room_id: field.room_select ? parseInt(field.room_select) : null,
room_number: field.room_number,
room_name: field.room_name,
room_fee: parseFloat(field.room_fee) || 0,
earnings: parseFloat(field.earnings) || 0,
type: 1, // 固定为佣金类型
remark: field.remark
};
if (!formData.user_id || formData.user_id <= 0) {
return layer.msg('请选择用户');
}
if (formData.earnings <= 0) {
return layer.msg('收益金额必须大于0');
}
coreHelper.Post("Api/SQEarnings/DoCreate", formData, function (res) {
if (res.code === 0) {
layer.msg('添加佣金成功');
layui.table.reloadData('LAY-app-CoreCmsUser-tableBox');
layer.close(index);
} else {
layer.msg(res.msg);
}
}); });
}) return false;
});
});
} }
, btn: ['确定', '取消'] , btn: ['确定', '取消']
, yes: function (index, layero) { , yes: function (index, layero) {
layero.contents().find("#LAY-app-CoreCmsUser-editBalanceForm-submit").click(); layero.contents().find("#LAY-app-SQEarnings-add-submit").click();
}
});
} else {
layer.msg(e.msg);
}
});
}
//执行编辑积分
function doEditPoint(obj) {
coreHelper.Post("Api/CoreCmsUser/GetEditPoint", { id: obj.data.id }, function (e) {
if (e.code === 0) {
admin.popup({
shadeClose: false,
title: '修改余额',
area: ['500px', '300px'],
id: 'LAY-popup-CoreCmsUser-EditPoint',
success: function (layero, index) {
view(this.id).render('user/userInfo/editPoint', { data: e.data }).done(function () {
//监听提交
form.on('submit(LAY-app-CoreCmsUser-editPointForm-submit)',
function (data) {
var field = data.field; //获取提交的字段
//提交 Ajax 成功后,关闭当前弹层并重载表格
coreHelper.Post("Api/CoreCmsUser/DoEditPoint", field, function (e) {
console.log(e)
if (e.code === 0) {
layui.table.reloadData('LAY-app-CoreCmsUser-tableBox'); //重载表格
layer.close(index); //再执行关闭
layer.msg(e.msg);
} else {
layer.msg(e.msg);
}
});
});
})
}
, btn: ['确定', '取消']
, yes: function (index, layero) {
layero.contents().find("#LAY-app-CoreCmsUser-editPointForm-submit").click();
} }
}); });
} else { } else {
@ -506,53 +519,6 @@
}); });
} }
//积分记录
$(document).on('click', '.point', function () {
var id = $(this).attr('data-id');
coreHelper.Post("Api/CoreCmsUser/GetDetailsPointLog", { id: id }, function (e) {
if (e.code === 0) {
admin.popup({
shadeClose: false,
title: '查看详情',
area: ['90%', '90%'],
id: 'LAY-popup-CoreCmsUser-details',
success: function (layero, index) {
view(this.id).render('user/userInfo/detailsPointLog', { data: e.data, id: id }).done(function () {
form.render();
});
}
, btn: ['取消']
, btnAlign: 'c'
});
} else {
layer.msg(e.msg);
}
});
});
//余额明细
$(document).on('click', '.balance', function () {
var id = $(this).attr('data-id');
coreHelper.Post("Api/CoreCmsUser/GetDetailsBalanceLog", { id: id }, function (e) {
if (e.code === 0) {
admin.popup({
shadeClose: false,
title: '查看详情',
area: ['90%', '90%'],
id: 'LAY-popup-CoreCmsUser-details',
success: function (layero, index) {
view(this.id).render('user/userInfo/detailsBalanceLog', { data: e.data, id: id }).done(function () {
form.render();
});
}
, btn: ['取消']
, btnAlign: 'c'
});
} else {
layer.msg(e.msg);
}
});
});
//执行单个删除 //执行单个删除
function doDelete(obj) { function doDelete(obj) {

View File

@ -1,4 +1,4 @@
using AutoMapper; using AutoMapper;
using CoreCms.Net.Auth.HttpContextUser; using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.IRepository.UnitOfWork; using CoreCms.Net.IRepository.UnitOfWork;
@ -1387,7 +1387,8 @@ public class SQController : ControllerBase
await _SQReservationParticipantsServices.UpdateAsync( await _SQReservationParticipantsServices.UpdateAsync(
it => new SQReservationParticipants it => new SQReservationParticipants
{ {
is_arrive = 1 is_arrive = 1,
check_reservation=DateTime.Now
}, },
it => it.reservation_id == dto.reservation_id && it.status == 0 && it.user_id == userId); it => it.reservation_id == dto.reservation_id && it.status == 0 && it.user_id == userId);

View File

@ -0,0 +1,266 @@
# 鸽子费审核功能需求文档
## 📌 功能概述
开发后台鸽子费审核功能,允许员工审核未按时赴约用户的鸽子费处理。审核通过则扣除鸽子费并分发给已赴约用户,审核未通过则退还鸽子费。
---
## 🎯 核心功能
### 1. 待审核列表展示
#### 1.1 筛选条件
- **用户状态**`is_arrive = 2`(未赴约,待审核)
- **预约状态**:预约未结束或已结束但未处理
- **鸽子费条件**`deposit_fee > 0`(有鸽子费的预约)
- **参与状态**`status = 0`(未退出)
#### 1.2 列表显示字段
| 字段 | 说明 |
|------|------|
| 预约ID | reservation_id |
| 预约标题 | reservation.title |
| 用户ID | user_id |
| 用户昵称 | 关联用户表获取 |
| 用户手机号 | 关联用户表获取 |
| 鸽子费金额 | reservation.deposit_fee |
| 预约时间 | reservation.start_time ~ end_time |
| 标记时间 | 发起者签到时间is_arrive=2的更新时间 |
| 操作按钮 | 审核通过 / 审核未通过 |
#### 1.3 列表规则
- 只显示有鸽子费(`deposit_fee > 0`)的未赴约用户
- 已审核通过的(`is_arrive = 3`)不显示在列表中
- 按标记时间倒序排列
---
## ✅ 审核通过流程
### 2.1 状态更新
- 将用户状态改为:`is_arrive = 3`(未赴约,已审核)
- 用户参与状态保持:`status = 0`(未退出,但已标记为未赴约)
### 2.2 鸽子费分配规则
#### 分配对象
- **已赴约且未退出的参与者**`is_arrive = 1` 且 `status = 0`
- **时间限制**:只计算在签到时间之前加入的参与者(`join_time < 签到时间`
- **排除对象**
- 未赴约的用户(`is_arrive = 2` 或 `is_arrive = 3`
- 已退出的用户(`status = 1`
- 签到时间之后加入的用户
#### 分配计算
```
鸽子费总额 = reservation.deposit_fee
参与人数 = 符合条件的已赴约用户数量(不包括发起者)
每人分得金额 = 鸽子费总额 / 参与人数(保留两位小数,四舍五入)
```
**注意**
- 如果参与人数为0鸽子费不分配但状态仍改为已审核
- 金额保留两位小数,使用四舍五入
- 分配后的总金额可能略有差异(因四舍五入),需要处理尾差
### 2.3 收益记录创建
为每个分到鸽子费的用户创建收益记录: 使用现有的收益服务
- **表**`SQEarningsRecord`
- **字段**
- `user_id`分到钱的用户ID
- `reservation_id`预约ID
- `room_id`预约的房间ID
- `room_number`:房间号
- `room_name`:房间名
- `room_fee`:预约的房费(可为空)
- `earnings`:分到的鸽子费金额
- `type`2鸽子费
- `remark``"未赴约用户鸽子费分配"`
- `create_time`:当前时间
- `operator_id`审核员工ID
### 2.4 用户收益更新
为每个分到鸽子费的用户更新:
- `CoreCmsUser.pending_earnings` += 分到的金额
### 2.5 消息通知
#### 2.5.1 给未赴约用户发送消息
- **接收人**:被扣除鸽子费的用户
- **标题**`"鸽子费扣除通知"`
- **内容**`"您未按时参加预约,鸽子费已扣除{金额}元。"`
- **消息类型**:系统通知(`message_type = 0`
- **关联业务**`related_type = 1`(组局),`related_id = reservation_id`
#### 2.5.2 给分到钱的用户发送消息
- **接收人**:每个分到鸽子费的用户
- **标题**`"鸽子费分配通知"`
- **内容**`"{未赴约用户昵称} 未能按时参加预约,您收到鸽子费{金额}元"`
- **消息类型**:系统通知(`message_type = 0`
- **关联业务**`related_type = 1`(组局),`related_id = reservation_id`
### 2.6 列表更新
- 审核通过后,该记录不再显示在待审核列表中(因为 `is_arrive = 3`
---
## ❌ 审核未通过流程
### 3.1 状态更新
- `is_arrive = 1`(已赴约)
- `status = 0`(正常,未退出)
- `is_refund = 3`(发起退款,由退款任务处理)
### 3.2 退款处理
- 不立即退款,只标记为 `is_refund = 3`
- 由现有的退款定时任务统一处理实际退款
### 3.3 消息通知
- **接收人**:被审核的用户
- **标题**`"鸽子费审核结果"`
- **内容**`"您的鸽子费已原路退回。"`
- **消息类型**:系统通知(`message_type = 0`
- **关联业务**`related_type = 1`(组局),`related_id = reservation_id`
### 3.4 列表更新
- 审核未通过后,该记录不再显示在待审核列表中(因为 `is_arrive = 1`
---
## 🔍 业务规则说明
### 4.1 时间限制说明
**"按照时间去处理一下,后面参与的人不算平均"** 的含义:
- **签到时间**:发起者执行签到操作的时间(`check_reservation` 接口调用时间)
- **目的**:防止在签到之后才加入的用户也能分到钱
### 4.2 特殊情况处理
1. **没有已赴约用户**
- 仍然将状态改为 `is_arrive = 3`
- 不分配鸽子费(鸽子费保留在系统中或按其他规则处理)
- 只给未赴约用户发送扣除通知
2. **只有发起者一人**
- 如果发起者已赴约,鸽子费不分配(因为没有其他参与者)
- 如果发起者未赴约,这种情况不应该出现(发起者不能标记自己未赴约)
3. **金额尾差处理**
- 由于四舍五入,分配总额可能略小于或大于原金额
- 建议:将尾差加给最后一个用户,或加给发起者
---
## 📋 后台管理功能
### 5.1 页面路径
- **建议路径**`/views/sq/sqreservations/pigeon-fee-audit.html`
- **或**:在现有的预约管理页面中新增一个标签页
### 5.2 功能按钮
- **审核通过**:点击后弹出确认框,确认后执行审核通过流程
- **审核未通过**:点击后弹出确认框(可选填写原因),确认后执行审核未通过流程
### 5.3 筛选功能
- 按预约ID筛选
- 按用户ID/昵称/手机号筛选
- 按预约时间范围筛选
- 按标记时间范围筛选
---
## 🗄️ 数据库影响
### 6.1 涉及的表
1. **SQReservationParticipants**:更新 `is_arrive`、`status`、`is_refund`
2. **SQEarningsRecord**:新增收益记录
3. **CoreCmsUser**:更新 `pending_earnings`
4. **SQMessage**:新增消息记录
### 6.2 事务要求
- 审核通过操作需要使用事务,确保:
- 状态更新成功
- 收益记录创建成功
- 用户收益更新成功
- 消息发送成功
- 任一环节失败,全部回滚
---
## 📝 接口设计建议
### 7.1 获取待审核列表
- **接口**`POST /api/SQReservations/GetPigeonFeeAuditList`
- **参数**:分页参数、筛选条件
- **返回**:待审核列表数据
### 7.2 审核通过
- **接口**`POST /api/SQReservations/ApprovePigeonFee`
- **参数**`{ participantId: int, reservationId: int }`
- **返回**:操作结果
### 7.3 审核未通过
- **接口**`POST /api/SQReservations/RejectPigeonFee`
- **参数**`{ participantId: int, reservationId: int, reason?: string }`
- **返回**:操作结果
---
## ⚠️ 注意事项
1. **并发控制**:同一用户的审核操作需要加锁,防止重复审核
2. **数据一致性**:所有涉及金额的操作必须使用事务
3. **审计日志**记录审核操作的操作员ID和时间
4. **消息发送**:消息发送失败不应影响审核流程(可异步处理或记录日志)(使用现有的消息服务)
5. **金额精度**:所有金额计算使用 `decimal(18,2)`,保留两位小数
---
## 🔄 业务流程总结
### 审核通过流程
```
员工查看待审核列表
→ 点击"审核通过"
→ 系统计算已赴约用户(按时间筛选)
→ 计算每人分得金额
→ 开启事务
→ 更新用户状态 is_arrive=3
→ 为每个分到钱的用户创建收益记录使用SQEarningsServices看有没有满足的方法
→ 更新每个用户的 pending_earnings
→ 发送消息给未赴约用户使用现有的消息服务SQMessageServices看有没有满足的方法
→ 发送消息给每个分到钱的用户使用现有的消息服务SQMessageServices看有没有满足的方法
→ 提交事务
→ 刷新列表(该记录不再显示)
```
### 审核未通过流程
```
员工查看待审核列表
→ 点击"审核未通过"
→ 开启事务
→ 更新用户状态 is_arrive=1, status=0, is_refund=3
→ 发送消息给用户使用现有的消息服务SQMessageServices看有没有满足的方法
→ 提交事务
→ 刷新列表(该记录不再显示)
→ 退款由定时任务处理
```
---
## ❓ 已确认问题
1. **签到时间的获取**:如何准确获取发起者签到的具体时间?是否需要在 `check_reservation` 接口中记录签到时间?
2. **尾差处理**:当分配金额因四舍五入产生尾差时,如何处理?是加给最后一个用户,还是加给发起者,还是忽略?(忽略,只保留两位小数)
3. **没有已赴约用户的情况**:如果所有参与者都未赴约,鸽子费如何处理?是保留在系统中,还是退还给未赴约用户?(如果发起者未签到,不处理,如果是发起者签到了,按现在的流程走,由员工去线下判断,审核)
4. **发起者未赴约**:如果发起者自己未赴约(这种情况是否可能?),如何处理?(不处理,发起者未签到,不处理)
5. **批量审核**:是否需要支持批量审核功能?(不需要)