This commit is contained in:
zpc 2025-12-26 16:08:55 +08:00
parent 4b069089ec
commit ce50875e8d
24 changed files with 5610 additions and 3156 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(tree:*)",
"Bash(find:*)",
"Bash(grep:*)"
]
}
}

744
PROJECT_DESCRIPTION.md Normal file
View File

@ -0,0 +1,744 @@
# 麻将组局预约小程序项目说明
## 项目概述
这是一个**麻将组局预约小程序**,帮助麻将爱好者在线上发起、加入麻将局,线下到店进行游戏。小程序提供了完整的预约流程管理,包括房间选择、预约发起、参与管理、签到评价等功能。
**技术栈:**
- **前端**: UniApp支持多端发布
- **后端**: .NET Core + SqlSugar ORM
- **数据库**: Microsoft SQL Server
---
## 核心功能模块
### 1. 首页预约列表
**文件位置:**
- 前端: `uniapp/mahjong_group/pages/index/index.vue`
- 后端: `server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs:170` (GetReservationList)
**功能描述:**
- 展示所有未结束的预约列表(按开始时间升序)
- 自动过滤黑名单用户发起的预约
- 分页加载,支持下拉刷新
- 卡片式展示预约信息(标题、时间、房间、人数、玩法等)
**关键逻辑:**
```sql
-- 查询未结束的预约status < 3end_time > now
-- 如果用户已登录,排除黑名单用户发起的预约
-- 按开始时间升序排列
```
---
### 2. 发起预约
**文件位置:**
- 前端: `uniapp/mahjong_group/pages/appointment/appointment-page.vue`
- 后端API: `SQController.cs:524` (AddSQReservation)
**预约流程:**
#### 第一步:选择房间和日期
- 页面: `book-room-page.vue`
- API: `GetRoomListWithSlotsNew` (获取房间列表及时段状态)
- 支持按**四个时段**查看房间可用性:
- 凌晨: 00:00-06:00
- 上午: 06:00-12:00
- 下午: 12:00-18:00
- 晚上: 18:00-00:00
#### 第二步:填写预约信息
填写内容包括:
**基本信息:**
- 组局名称
- 人数最多4人
- 玩法类型(血战、血流成河等)
- 具体规则(几番起胡、几倍封顶等)
- 其他补充说明
**参与者限制:**
- 是否禁烟(可选)
- 性别限制(不限/男/女)
- 年龄范围(最小年龄-最大年龄)
- 信誉要求0.0-5.0分)
**鸽子费(押金):**
- 固定金额0元、5元、10元、20元
- 自定义金额0-50元
- 说明: 参与者需缴纳押金,爽约者押金由到场者平分,预约完成后全额返还
#### 第三步:提交预约
1. 调用 `canCreateSQReservation` 验证是否可以创建
2. 如有鸽子费,调用 `usePay` 发起微信支付
3. 调用 `addSQReservation` 创建预约记录
4. 创建发起者的参与记录role=1
**验证规则:**
- 房间是否存在且可用
- 时间段是否已被预约
- 用户是否有时间冲突的预约
- 房间在该时段是否有不可用时间
---
### 3. 加入预约
**后端API:** `SQController.cs:804` (JoinReservation)
**加入流程:**
1. 校验预约是否存在且未结束
2. 校验用户是否已加入
3. 校验是否为"独享模式"(无需组局)
4. 校验用户是否符合参与条件:
- 信誉分是否达到要求
- 性别是否符合限制
- 年龄是否在范围内
5. 校验是否有时间冲突的预约
6. 校验预约是否已满员
7. 如有鸽子费,验证支付信息
8. 创建参与者记录role=0
**重要提示:**
- 参与者需满足发起者设置的所有限制条件
- 加入预约后可以取消但在开始前30分钟内无法取消
- 如有鸽子费需先完成支付
---
### 4. 预约签到
**后端API:** `SQController.cs:1207` (CheckInReservation)
**签到权限:** 仅发起者可操作
**签到时机:** 预约开始后,由发起者确认实际到场人员
**签到流程:**
1. 发起者在预约详情页点击"签到"按钮
2. 勾选实际到场的参与者(发起者默认到场)
3. 提交签到
**签到效果:**
- 预约状态变更为"进行中"status=2
- 到场人员标记为`is_arrive=1`
- 未到场人员标记为`is_arrive=2`并退出预约
- 爽约者扣除0.5信誉分,增加鸽子次数
- 到场者增加0.2信誉分最高5.0
- 如有鸽子费,到场者标记为"发起退款"状态
**信誉系统:**
```javascript
// 爽约处罚
credit_score -= 0.5
dove_count++
// 守约奖励(信誉<5.0时
credit_score += 0.2 (最高5.0)
```
---
### 5. 评价系统
**后端API:**
- 获取评价列表: `SQController.cs:252` (GetEvaluateServices)
- 添加评价: `SQController.cs:330` (AddEvaluateServices)
**评价条件:**
- 预约已完成且已签到
- 只能评价实际到场的参与者
- 每个参与者只能被评价一次
**评价维度:**
- 游戏水平play_level: 1-5分
- 技能水平skills_level: 1-5分
**评价计算:**
用户的最终评分采用加权平均:
```javascript
// 初始值为4分
play_level = (sum(评价分数) + 4) / (评价次数 + 1)
skills_level = (sum(评价分数) + 4) / (评价次数 + 1)
// 如果只有1次评价分母+1避免偏差
if (评价次数 == 1) {
分母 = 评价次数 + 2
}
```
**评价作用:**
- 其他用户可在预约详情看到参与者的平均评分
- 评分影响用户在列表中的展示排序
- 高评分用户更容易被其他人接受加入
---
### 6. 取消预约
**后端API:** `SQController.cs:1036` (CancelReservation)
**取消规则:**
**发起者取消:**
- 预约开始前30分钟内无法取消
- 取消后预约状态变为"已取消"status=4
- 所有参与者自动退出
- 如有押金,已支付者发起退款
- 通知所有参与者"发起者解散组局"
**参与者取消:**
- 预约开始前30分钟内无法取消
- 只退出自己,不影响其他人
- 如有押金,发起退款
**退款状态说明:**
- `is_refund=1`: 待支付
- `is_refund=2`: 已支付
- `is_refund=3`: 待退款(发起退款流程)
- `is_refund=4`: 已退款
- `is_refund=5`: 退款异常
---
### 7. 我的预约记录
**后端API:** `SQController.cs:120` (GetMyReservation)
**分类查看:**
- `type=0`: 我参与的预约role=0
- `type=1`: 我发起的预约role=1
**记录状态:**
- 待开始status=0
- 已锁定status=1- 人满
- 进行中status=2- 已签到
- 已结束status=3
- 已取消status=4
**我正在进行的预约:**
API: `GetMyUseReservation`
- 查询未取消且未结束的预约
- 按状态排序:进行中 > 已锁定 > 待开始 > 已结束
---
### 8. 黑名单功能
**说明:**
- 用户可将不友好的参与者加入黑名单
- 首页自动过滤黑名单用户发起的预约
- 黑名单用户无法加入我发起的预约(待实现)
**相关表:** `CoreCmsUserBlacklist`
---
### 9. 消息系统
**后端API:**
- 获取消息列表: `SQController.cs:1778` (GetMessageList)
- 获取未读数量: `SQController.cs:1814` (GetUnreadCount)
- 全部标记已读: `SQController.cs:1850` (MarkAllAsRead)
**消息类型:**
- 系统消息:预约相关通知(组局成功、被取消等)
- 私信消息(暂未实现)
**消息状态:**
- 未读:红点提示
- 已读:正常显示
**相关表:**
- `SQMessage`: 消息表
- `SQMessageRead`: 已读记录表
---
### 10. 收益系统
**后端API:**
- 获取收益统计: `SQController.cs:1897` (GetEarningsSummary)
- 获取收益记录: `SQController.cs:1970` (GetEarningsRecordList)
- 申请提现: `SQController.cs:2050` (ApplyWithdraw)
**收益来源:**
- 发起预约的抽成(具体规则待配置)
- 爽约者的鸽子费分成
**提现规则:**
- 最低提现金额0.01元
- 提现到账时间3-5个工作日
- 提现记录可查询
**相关表:**
- `SQEarningsRecord`: 收益记录表
- `SQWithdrawRecord`: 提现记录表
---
## 数据库设计
### 核心数据表
#### 1. SQReservations (预约表)
主要字段:
```sql
id -- 预约ID
room_id -- 房间ID
room_name -- 房间名称
start_time -- 开始时间
end_time -- 结束时间
duration_minutes -- 时长(分钟)
title -- 组局名称
game_type -- 游戏类型
game_rule -- 游戏规则
player_count -- 需要人数
status -- 状态0待开始 1已锁定 2进行中 3已结束 4已取消
deposit_fee -- 押金费用
credit_limit -- 最低信誉要求
gender_limit -- 性别限制0不限 1男 2女
min_age -- 最小年龄
max_age -- 最大年龄
is_smoking -- 是否禁烟
latest_arrival_time -- 最晚到店时间
extra_info -- 其他说明
is_solo_mode -- 是否独享模式(无需组局)
created_at -- 创建时间
updated_at -- 更新时间
```
#### 2. SQReservationParticipants (参与者表)
主要字段:
```sql
id -- 参与记录ID
reservation_id -- 预约ID
user_id -- 用户ID
role -- 角色0参与者 1发起者
status -- 状态0正常 1已退出
join_time -- 加入时间
quit_time -- 退出时间
is_arrive -- 是否到场0未签到 1到场 2未到场
check_reservation -- 签到时间
is_refund -- 退款状态1待支付 2已支付 3待退款 4已退款 5异常
paymentId -- 支付订单号
important_data -- 重要数据JSON
```
#### 3. SQRooms (房间表)
主要字段:
```sql
id -- 房间ID
name -- 房间名称
capacity -- 容量(人数)
price_per_hour -- 每小时价格
description -- 描述
image_url -- 图片
status -- 状态true可用 false不可用
created_at -- 创建时间
```
#### 4. SQReservationEvaluate (评价表)
主要字段:
```sql
id -- 评价ID
reservation_id -- 预约ID
user_id -- 评价人ID
to_user_id -- 被评价人ID
role -- 被评价人角色
play_level -- 游戏水平评分
skills_level -- 技能水平评分
created_at -- 评价时间
```
#### 5. SQReservationReputation (声誉记录表)
主要字段:
```sql
id -- 记录ID
user_id -- 用户ID
reservation_id -- 相关预约ID
reputation_value -- 声誉变化值±0.5、±0.2等)
remark -- 变化原因
created_at -- 记录时间
```
#### 6. SQRoomUnavailableTimes (房间不可用时间表)
主要字段:
```sql
id -- 记录ID
room_id -- 房间ID
start_time -- 不可用开始时间
end_time -- 不可用结束时间
reason -- 原因
created_at -- 创建时间
```
#### 7. SQMessage (消息表)
主要字段:
```sql
id -- 消息ID
user_id -- 接收用户ID0表示全体用户
title -- 消息标题
content -- 消息内容
message_type -- 消息类型0系统 1私信
created_at -- 创建时间
```
#### 8. SQEarningsRecord (收益记录表)
主要字段:
```sql
id -- 记录ID
user_id -- 用户ID
reservation_id -- 相关预约ID
amount -- 收益金额
type -- 收益类型
description -- 描述
created_at -- 创建时间
```
---
## 前端页面结构
### TabBar底部导航
1. **首页** (`pages/index/index`)
- 预约列表展示
- 支持下拉刷新、上拉加载
- 点击卡片查看详情/加入预约
2. **预约** (`pages/appointment/book-room-page`)
- 选择房间和日期
- 查看房间时段可用性
- 进入预约表单
3. **我的** (`pages/me/me-page`)
- 用户信息展示
- 预约记录入口
- 我的收益入口
- 消息通知入口
### 主要功能页面
#### 预约相关
- `pages/appointment/book-room-page.vue` - 选择房间页面
- `pages/appointment/appointment-page.vue` - 发起预约页面
#### 个人中心
- `pages/me/appointment-record-page.vue` - 预约记录
- `pages/me/my-earnings-page.vue` - 我的收益
- `pages/me/my-message-page.vue` - 消息列表
- `pages/me/my-record.vue` - 历史记录
- `pages/me/blacklist-page.vue` - 黑名单管理
- `pages/me/edit-info.vue` - 编辑个人信息
- `pages/me/login.vue` - 登录页面
#### 其他页面
- `pages/other/agreement.vue` - 用户协议
- `pages/other/payment-records.vue` - 支付记录
- `pages/other/faq.vue` - 常见问题
---
## API接口汇总
### 预约相关接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取预约列表 | `api/sq/GetReservationList` | GET | 无需 | 首页预约列表 |
| 获取预约详情 | `api/sq/GetReservationDetail` | GET | 无需 | 根据ID获取详情 |
| 我的预约记录 | `api/sq/GetMyReservation` | GET | 需要 | 我参与/发起的预约 |
| 正在进行的预约 | `api/sq/GetMyUseReservation` | GET | 需要 | 未结束的预约 |
| 验证是否可创建 | `api/sq/CanCreateSQReservation` | POST | 需要 | 创建预约前验证 |
| 创建预约 | `api/sq/AddSQReservation` | POST | 需要 | 发起新预约 |
| 加入预约 | `api/sq/JoinReservation` | POST | 需要 | 参与现有预约 |
| 取消预约 | `api/sq/CancelReservation` | POST | 需要 | 发起者/参与者取消 |
| 预约签到 | `api/sq/CheckInReservation` | POST | 需要 | 发起者签到确认 |
### 房间相关接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取可选日期 | `api/sq/GetAvailableDates` | GET | 无需 | 今天+未来6天 |
| 获取房间列表 | `api/sq/GetRoomListWithSlotsNew` | GET | 无需 | 按时段显示房间状态 |
| 获取房间详情 | `api/sq/GetRoomDetail` | GET | 无需 | 房间信息及可用时段 |
| 获取可预约房间 | `api/sq/GetReservationRoomList` | GET | 无需 | 指定时间段可预约房间 |
### 评价相关接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取预约评价 | `api/sq/GetEvaluateServices` | GET | 需要 | 获取可评价参与者 |
| 添加评价 | `api/sq/AddEvaluateServices` | POST | 需要 | 评价参与者 |
| 获取声誉记录 | `api/sq/GetReputationByUser` | GET | 需要 | 我的信誉变化记录 |
| 获取评价给我的 | `api/sq/GetEvaluateToMe` | GET | 需要 | 别人给我的评价 |
### 消息相关接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取消息列表 | `api/sq/GetMessageList` | GET | 需要 | 站内信列表 |
| 获取未读数量 | `api/sq/GetUnreadCount` | GET | 需要 | 未读消息数量 |
| 全部标记已读 | `api/sq/MarkAllAsRead` | POST | 需要 | 标记所有消息已读 |
### 收益相关接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取收益统计 | `api/sq/GetEarningsSummary` | GET | 需要 | 总收益、可提现等 |
| 获取收益记录 | `api/sq/GetEarningsRecordList` | POST | 需要 | 收益明细列表 |
| 获取提现记录 | `api/sq/GetWithdrawRecordList` | POST | 需要 | 提现记录列表 |
| 申请提现 | `api/sq/ApplyWithdraw` | POST | 需要 | 发起提现申请 |
| 获取收益规则 | `api/sq/GetEarningsRule` | GET | 无需 | 收益规则说明 |
### 其他接口
| 接口名称 | 路径 | 方法 | 权限 | 说明 |
|---------|------|------|------|------|
| 获取支付记录 | `api/sq/GetPaymentRecords` | GET | 需要 | 鸽子费支付记录 |
| 获取营业时间 | `api/sq/GetBusinessHours` | GET | 无需 | 店铺营业时间配置 |
---
## 业务流程图
### 完整预约流程
```
用户浏览首页预约列表
选择"发起预约"或"加入预约"
[发起预约流程] [加入预约流程]
↓ ↓
选择房间和日期 查看预约详情
↓ ↓
选择时段 检查参与条件
↓ ↓
填写组局信息 支付鸽子费(如有)
↓ ↓
设置参与限制 加入成功
↓ ↓
设置鸽子费 等待预约开始
↓ ↓
支付鸽子费(如有) 收到开始通知
发起成功
等待参与者加入
人满/时间到达开始
发起者签到确认到场人员
预约进行中
预约时间结束
参与者互相评价
预约完成(鸽子费退还)
```
### 签到流程详解
```
预约开始时间到达
发起者收到签到通知
发起者打开预约详情
点击"签到"按钮
勾选实际到场的参与者
提交签到
系统处理:
- 预约状态→进行中
- 到场者is_arrive=1信誉+0.2
- 未到场者is_arrive=2信誉-0.5,鸽子次数+1
- 未到场者押金→到场者平分(待定时任务处理)
- 到场者押金→发起退款
签到完成
预约正常进行
```
### 取消预约流程
```
用户查看我的预约
选择要取消的预约
点击"取消预约"
检查取消条件:
- 是否在开始前30分钟
- 是否已开始(只有发起者可取消已开始的)
- 是否已结束或已取消
[发起者取消] [参与者取消]
↓ ↓
预约状态→已取消 只退出自己
↓ ↓
所有参与者→已退出 预约继续有效
↓ ↓
发起押金退款 发起押金退款(自己的)
↓ ↓
通知所有参与者 无需通知
↓ ↓
取消完成
```
---
## 业务规则总结
### 时间规则
1. 预约可选时间:今天 + 未来6天
2. 时段划分:凌晨(0-6h)、上午(6-12h)、下午(12-18h)、晚上(18-24h)
3. 营业时间09:00-23:00
4. 取消限制开始前30分钟内无法取消
### 人数规则
1. 每个预约最少1人独享模式最多4人
2. 独享模式player_count=1不接受其他人加入
3. 人满后预约状态→已锁定status=1
### 押金规则
1. 押金范围0-50元
2. 押金用途:防止爽约
3. 退款时机:
- 签到后到场者:全额退还
- 爽约者:押金被扣除,由到场者平分
- 预约取消:全额退还
### 信誉规则
1. 初始信誉5.0分(满分)
2. 守约奖励:+0.2分/次最高5.0
3. 爽约惩罚:-0.5分/次
4. 信誉作用:发起者可设置最低信誉要求
### 评价规则
1. 评价时机:预约完成后
2. 评价对象:实际到场的参与者
3. 评价维度游戏水平、技能水平各1-5分
4. 评价次数:每个参与者只能被评价一次
5. 平均分计算:(所有评价之和 + 4) / (评价次数 + 1)
### 参与限制规则
1. 性别限制:不限/男/女
2. 年龄限制:最小-最大年龄
3. 信誉限制:最低信誉要求
4. 时间冲突检查:不能同时参与多个时间重叠的预约
---
## 定时任务(推测)
根据代码逻辑,应该有以下定时任务:
1. **押金退款任务**
- 扫描 `is_refund=3`(待退款)的记录
- 调用微信退款API
- 更新退款状态为 `is_refund=4`(已退款)
2. **预约自动结束任务**
- 扫描 `end_time < now``status=2`(进行中)的预约
- 更新状态为 `status=3`(已结束)
3. **预约失败通知任务**
- 扫描开始时间到达但人数不足的预约
- 通知参与者"组局失败"
- 退还所有押金
---
## 项目特色
### 1. 完善的信誉体系
- 守约加分、爽约扣分
- 鸽子次数统计
- 参与条件限制
### 2. 灵活的押金机制
- 防止恶意爽约
- 爽约者押金补偿到场者
- 守约者全额退还
### 3. 双向评价系统
- 游戏水平、技能水平分开评价
- 加权平均算法避免偏差
- 评价影响用户可信度
### 4. 智能房间管理
- 按时段展示房间可用性
- 自动过滤已预约/不可用时段
- 支持房间不可用时间配置
### 5. 黑名单机制
- 避免与不友好用户组局
- 首页自动过滤黑名单预约
---
## 注意事项
### 代码特点
1. 后端代码是从商城系统改造的,包含一些未使用的表和字段
2. 核心预约功能集中在 `SQController`
3. 前端使用 UniApp 开发,支持多端发布
4. 数据库使用 SqlSugar ORM部分查询直接使用原生SQL
### 需要改进的地方
1. 部分业务逻辑写在 Controller 中,应该抽离到 Service 层
2. 定时任务的具体实现需要补充
3. 消息推送功能需要完善(微信模板消息/订阅消息)
4. 收益分配规则需要明确配置
5. 单元测试和接口文档需要完善
---
## 部署建议
### 环境要求
- .NET Core 6.0+
- SQL Server 2016+
- Redis如需缓存
### 配置项
1. 数据库连接字符串
2. 微信小程序配置AppId、AppSecret
3. 微信支付配置(商户号、密钥)
4. 营业时间配置
5. 收益规则配置
### 运行步骤
1. 恢复数据库(执行建表脚本)
2. 配置 `appsettings.json`
3. 编译后端项目
4. 部署到IIS或使用Kestrel
5. 配置前端小程序AppId
6. 编译上传小程序
---
## 总结
这是一个功能完善的**麻将组局预约小程序**,核心流程包括:
1. 用户浏览首页预约列表
2. 发起/加入预约
3. 支付鸽子费(押金)
4. 预约开始后发起者签到
5. 预约完成后互相评价
6. 押金退还、收益分配
项目采用前后端分离架构,数据库设计合理,业务逻辑清晰。通过信誉体系、押金机制、评价系统等功能,有效防止了恶意爽约问题,提高了用户参与组局的积极性和可靠性。

View File

@ -1,86 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 广告api控制器
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class AdvertController : ControllerBase
{
private IHttpContextUser _user;
private readonly ICoreCmsArticleServices _articleServices;
private readonly ICoreCmsAdvertPositionServices _advertPositionServices;
private readonly ICoreCmsAdvertisementServices _advertisementServices;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="user"></param>
/// <param name="articleServices"></param>
/// <param name="advertPositionServices"></param>
/// <param name="advertisementServices"></param>
public AdvertController(IHttpContextUser user
, ICoreCmsArticleServices articleServices
, ICoreCmsAdvertPositionServices advertPositionServices
, ICoreCmsAdvertisementServices advertisementServices
)
{
_user = user;
_articleServices = articleServices;
_advertPositionServices = advertPositionServices;
_advertisementServices = advertisementServices;
}
#region 广=============================================================================
/// <summary>
/// 获取广告列表
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
[HttpGet]
public async Task<WebApiDto> GetAdvertList([FromQuery] string code)
{
if (string.IsNullOrEmpty(code))
{
return null;
}
var jm = new WebApiDto();
var list = await _advertisementServices.QueryListByClauseAsync(p => p.code == code, p => p.createTime, OrderByType.Desc);
jm.Code = 0;
jm.Data = list;
return jm;
}
#endregion
}
}

View File

@ -1,441 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.Entities.Expression;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Utility.Extensions;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 代理请求接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class AgentController : ControllerBase
{
private IHttpContextUser _user;
private readonly ICoreCmsAgentServices _agentServices;
private readonly ICoreCmsAgentOrderServices _agentOrderServices;
private readonly ICoreCmsAgentGoodsServices _agentGoodsServices;
private readonly ICoreCmsSettingServices _settingServices;
private readonly ICoreCmsUserServices _userServices;
private readonly ICoreCmsGoodsServices _goodsServices;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="user"></param>
/// <param name="agentServices"></param>
/// <param name="settingServices"></param>
/// <param name="agentOrderServices"></param>
/// <param name="userServices"></param>
/// <param name="goodsServices"></param>
/// <param name="agentGoodsServices"></param>
public AgentController(IHttpContextUser user, ICoreCmsAgentServices agentServices, ICoreCmsSettingServices settingServices, ICoreCmsAgentOrderServices agentOrderServices, ICoreCmsUserServices userServices, ICoreCmsGoodsServices goodsServices, ICoreCmsAgentGoodsServices agentGoodsServices)
{
_user = user;
_agentServices = agentServices;
_settingServices = settingServices;
_agentOrderServices = agentOrderServices;
_userServices = userServices;
_goodsServices = goodsServices;
_agentGoodsServices = agentGoodsServices;
}
//公共接口====================================================================================================
#region
/// <summary>
/// 获取店铺信息
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetStoreInfo([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
if (entity.id == 0)
{
jm.msg = "店铺信息丢失";
return jm;
}
var store = UserHelper.GetUserIdByShareCode(entity.id);
if (store <= 0)
{
jm.msg = "店铺信息丢失";
return jm;
}
jm = await _agentServices.GetStore(store);
return jm;
}
#endregion
#region ============================================================
/// <summary>
/// 根据查询条件获取分页数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsPageList([FromBody] FMPageByWhereOrder entity)
{
var jm = new WebApiCallBack();
var where = PredicateBuilder.True<CoreCmsGoods>();
where = where.And(p => p.isDel == false);
where = where.And(p => p.isMarketable == true);
var className = string.Empty;
if (!string.IsNullOrEmpty(entity.where))
{
var obj = JsonConvert.DeserializeAnonymousType(entity.where, new
{
priceFrom = "",
priceTo = "",
catId = "",
brandId = "",
labelId = "",
searchName = "",
});
if (!string.IsNullOrEmpty(obj.priceFrom))
{
var priceF = obj.priceFrom.ObjectToDouble(0);
if (priceF >= 0)
{
var f = Convert.ToDecimal(priceF);
where = where.And(p => p.price >= f);
}
}
if (!string.IsNullOrEmpty(obj.priceTo))
{
var priceT = obj.priceTo.ObjectToDouble(0);
if (priceT >= 0)
{
var f = Convert.ToDecimal(priceT);
where = where.And(p => p.price <= f);
}
}
if (!string.IsNullOrEmpty(obj.brandId))
{
var brandId = obj.brandId.ObjectToInt(0);
if (brandId >= 0)
{
where = where.And(p => p.brandId == brandId);
}
}
if (!string.IsNullOrEmpty(obj.labelId))
{
var brandId = obj.brandId.ObjectToInt(0);
if (brandId >= 0)
{
where = where.And(p => p.brandId == brandId);
}
}
if (!string.IsNullOrEmpty(obj.searchName))
{
where = where.And(p => p.name.Contains(obj.searchName));
}
}
var orderBy = " isRecommend desc,isHot desc";
if (!string.IsNullOrEmpty(entity.order))
{
orderBy += "," + entity.order;
}
var list = await _goodsServices.QueryAgentGoodsPageAsync(where, orderBy, entity.page, entity.limit, false);
if (list.Any())
{
foreach (var goods in list)
{
goods.images = !string.IsNullOrEmpty(goods.images) ? goods.images.Split(",")[0] : "/static/images/common/empty.png";
}
}
//返回数据
jm.status = true;
jm.data = new
{
list,
className,
entity.page,
list.TotalCount,
list.TotalPages,
entity.limit,
entity.where,
entity.order,
};
jm.msg = "数据调用成功!";
return jm;
}
#endregion
//验证接口====================================================================================================
#region
/// <summary>
/// 查询用户是否可以成为代理商
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> Info()
{
var jm = await _agentServices.GetInfo(_user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 申请成为代理商接口
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> ApplyAgent([FromBody] FMAgentApply entity)
{
var jm = new WebApiCallBack();
if (entity.agreement != "on")
{
jm.msg = "请勾选代理商协议";
return jm;
}
var iData = new CoreCmsAgent();
iData.mobile = entity.mobile;
iData.name = entity.name;
iData.weixin = entity.weixin;
iData.qq = entity.qq;
jm = await _agentServices.AddData(iData, _user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 获取我的下级用户数量
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetTeamSum()
{
var jm = new WebApiCallBack();
//发展人数
var first = await _userServices.QueryChildCountAsync(_user.ID, 1);
//订单数
var second = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID);
//当月发展人数
var monthFirst = await _userServices.QueryChildCountAsync(_user.ID, 1, true);
DateTime dt = DateTime.Now;
//本月第一天时间
DateTime dtFirst = dt.AddDays(1 - (dt.Day));
dtFirst = new DateTime(dtFirst.Year, dtFirst.Month, dtFirst.Day, 0, 0, 0);
//获得某年某月的天数
int year = dt.Date.Year;
int month = dt.Date.Month;
int dayCount = DateTime.DaysInMonth(year, month);
//本月最后一天时间
DateTime dtLast = dtFirst.AddDays(dayCount - 1);
var monthSecond = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID && p.createTime > dtFirst && p.createTime < dtLast, true);
jm.status = true;
jm.data = new
{
count = first,
first,
second,
monthCount = monthFirst,
monthFirst,
monthSecond
};
return jm;
}
#endregion
#region
/// <summary>
/// 获取我的订单统计
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetOrderSum()
{
var jm = new WebApiCallBack();
DateTime dt = DateTime.Now;
//本月第一天时间
DateTime dtFirst = dt.AddDays(1 - (dt.Day));
dtFirst = new DateTime(dtFirst.Year, dtFirst.Month, dtFirst.Day, 0, 0, 0);
//获得某年某月的天数
int dayCount = DateTime.DaysInMonth(dt.Date.Year, dt.Date.Month);
//本月最后一天时间
DateTime dtLast = dtFirst.AddDays(dayCount - 1);
//全部订单
var allOrder = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID, true);
//代购订单
var procurementServiceOrder = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID && p.buyUserId == _user.ID, true);
//推广订单
var customerOrder = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID && p.buyUserId != _user.ID, true);
//本月订单
var monthOrder = await _agentOrderServices.GetCountAsync(p => p.userId == _user.ID && p.createTime > dtFirst && p.createTime < dtLast, true);
//全部订单金额
var allOrderMoney = await _agentOrderServices.GetSumAsync(p => p.userId == _user.ID, p => p.amount, true);
//代购订单金额
var procurementServiceOrderMoney = await _agentOrderServices.GetSumAsync(p => p.userId == _user.ID && p.buyUserId == _user.ID, p => p.amount, true);
//推广订单金额
var customerOrderMoney = await _agentOrderServices.GetSumAsync(p => p.userId == _user.ID && p.buyUserId != _user.ID, p => p.amount, true);
//本月订单金额
var monthOrderMoney = await _agentOrderServices.GetSumAsync(p => p.userId == _user.ID && p.createTime > dtFirst && p.createTime < dtLast, p => p.amount, true);
jm.status = true;
jm.data = new
{
allOrder,
procurementServiceOrder,
customerOrder,
monthOrder,
allOrderMoney,
procurementServiceOrderMoney,
customerOrderMoney,
monthOrderMoney
};
return jm;
}
#endregion
#region 广
/// <summary>
/// 我推广的订单
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> MyOrder([FromBody] FMPageByIntId entity)
{
var jm = await _agentServices.GetMyOrderList(_user.ID, entity.page, entity.limit, entity.id);
return jm;
}
#endregion
#region
/// <summary>
/// 店铺设置
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> SetStore([FromBody] FMSetAgentStorePost entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.storeName))
{
jm.msg = "请填写店铺名称";
return jm;
}
if (string.IsNullOrEmpty(entity.storeLogo))
{
jm.msg = "请上传店铺logo";
return jm;
}
if (string.IsNullOrEmpty(entity.storeBanner))
{
jm.msg = "请上传店铺banner";
return jm;
}
var info = await _agentServices.QueryByClauseAsync(p => p.userId == _user.ID);
if (info != null)
{
info.storeLogo = entity.storeLogo;
info.storeBanner = entity.storeBanner;
info.storeDesc = entity.storeDesc;
info.storeName = entity.storeName;
await _agentServices.UpdateAsync(info);
}
jm.status = true;
jm.msg = "保存成功";
return jm;
}
#endregion
#region
/// <summary>
/// 获取代理商排行
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetAgentRanking([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
var list = await _agentServices.QueryRankingPageAsync(entity.page, entity.limit);
jm.status = true;
jm.data = new
{
data = list,
list.HasNextPage,
list.HasPreviousPage,
list.PageIndex,
list.PageSize,
list.TotalPages,
list.TotalCount,
};
return jm;
}
#endregion
}
}

View File

@ -1,130 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 文章api控制器
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class ArticleController : ControllerBase
{
private IHttpContextUser _user;
private readonly ICoreCmsArticleServices _articleServices;
private readonly ICoreCmsArticleTypeServices _articleTypeServices;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="user"></param>
/// <param name="articleServices"></param>
/// <param name="articleTypeServices"></param>
public ArticleController(IHttpContextUser user, ICoreCmsArticleServices articleServices, ICoreCmsArticleTypeServices articleTypeServices)
{
_user = user;
_articleServices = articleServices;
_articleTypeServices = articleTypeServices;
}
#region
/// <summary>
/// 获取通知列表
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> NoticeList([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
var list = await _articleServices.QueryPageAsync(p => p.isDel == false, p => p.createTime, OrderByType.Desc,
entity.page, entity.limit);
jm.status = true;
jm.data = list;
return jm;
}
#endregion
#region
/// <summary>
/// 获取文章列表
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetArticleList([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
var list = await _articleServices.QueryPageAsync(p => p.isDel == false && p.typeId == entity.id, p => p.createTime, OrderByType.Desc,
entity.page, entity.limit);
var articleType = await _articleTypeServices.QueryAsync();
var typeName = string.Empty;
if (articleType.Any())
{
var type = articleType.Find(p => p.id == entity.id);
typeName = type != null ? type.name : "";
}
jm.status = true;
jm.data = new
{
list,
articleType,
type_name = typeName,
count = list.TotalCount
};
return jm;
}
#endregion
/// <summary>
/// 获取单个文章内容
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<WebApiCallBack> GetArticleDetail([FromQuery] int id)
{
var jm = new WebApiCallBack();
var model = await _articleServices.ArticleDetail(id);
if (model == null)
{
jm.msg = "数据获取失败";
return jm;
}
jm.status = true;
jm.data = model;
return jm;
}
}
}

View File

@ -1,142 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Model.ViewModels.DTO;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 购物车操作
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class CartController : ControllerBase
{
private readonly IHttpContextUser _user;
private readonly ICoreCmsCartServices _cartServices;
/// <summary>
/// 构造函数
/// </summary>
public CartController(IHttpContextUser user, ICoreCmsCartServices cartServices)
{
_user = user;
_cartServices = cartServices;
}
//公共接口====================================================================================================
//验证接口====================================================================================================
#region
/// <summary>
/// 添加单个货品到购物车
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> AddCart([FromBody] FMCartAdd entity)
{
var jm = await _cartServices.Add(_user.ID, entity.ProductId, entity.Nums, entity.type, entity.cartType, entity.objectId);
return jm;
}
#endregion
#region ======================================================================
/// <summary>
/// 获取购物车列表
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetList([FromBody] FMCartGetList entity)
{
var ids = CommonHelper.StringToIntArray(entity.ids);
//判断免费运费
var freeFreight = entity.receiptType != 1;
//获取数据
var jm = await _cartServices.GetCartInfos(_user.ID, ids, entity.type, entity.areaId, entity.point, entity.couponCode, freeFreight, entity.receiptType, entity.objectId);
return jm;
}
#endregion ======================================================================
#region
/// <summary>
/// 获取购物车列表
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> DoDelete([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
if (entity.id <= 0)
{
jm.msg = "请提交要删除的货品";
return jm;
}
jm = await _cartServices.DeleteByIdsAsync(entity.id, _user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 设置购物车商品数量
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> SetCartNum([FromBody] FMSetCartNum entity)
{
var jm = await _cartServices.SetCartNum(entity.id, entity.nums, _user.ID, 2, 1);
return jm;
}
#endregion
#region 使==================================================
/// <summary>
/// 根据提交的数据判断哪些购物券可以使用
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetCartAvailableCoupon([FromBody] FMCouponForUserCouponPost entity)
{
var ids = CommonHelper.StringToIntArray(entity.ids);
var jm = await _cartServices.GetCartAvailableCoupon(_user.ID, ids);
return jm;
}
#endregion 使==================================================
}
}

View File

@ -1,253 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IRepository.UnitOfWork;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 优惠券接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class CouponController : ControllerBase
{
private readonly IHttpContextUser _user;
private readonly ICoreCmsCouponServices _couponServices;
private readonly ICoreCmsPromotionServices _promotionServices;
private readonly IUnitOfWork _unionOfWork;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="user"></param>
/// <param name="couponServices"></param>
/// <param name="promotionServices"></param>
/// <param name="unionOfWork"></param>
public CouponController(IHttpContextUser user
, ICoreCmsCouponServices couponServices, ICoreCmsPromotionServices promotionServices, IUnitOfWork unionOfWork)
{
_user = user;
_couponServices = couponServices;
_promotionServices = promotionServices;
_unionOfWork = unionOfWork;
}
//公共接口====================================================================================================
#region ==================================================
/// <summary>
/// 获取 可领取的优惠券
/// </summary>
/// <returns></returns>
[HttpPost]
//[Authorize]
public async Task<WebApiCallBack> CouponList([FromBody] FMCouponForUserCouponPost entity)
{
var jm = new WebApiCallBack() { msg = "获取失败" };
var list = await _promotionServices.GetReceiveCouponList(entity.page, entity.limit);
jm.status = true;
jm.data = list;
jm.msg = "获取成功";
jm.otherData = new
{
totalCount = 0,
totalPages = 0,
};
if (list != null && list.Any())
{
jm.data = list;
jm.otherData = new
{
list.TotalCount,
list.TotalPages
};
}
return jm;
}
#endregion
//验证接口====================================================================================================
#region ==================================================
/// <summary>
/// 获取优惠券 详情
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> CouponDetail([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack() { msg = "获取失败" };
if (entity.id == 0)
{
jm.status = false;
jm.msg = GlobalErrorCodeVars.Code15006;
return jm;
}
var promotionModel = await _promotionServices.QueryByClauseAsync(p => p.id == entity.id);
if (promotionModel != null)
{
jm.status = true;
jm.data = promotionModel;
jm.msg = "获取成功";
}
return jm;
}
#endregion
#region ==================================================
/// <summary>
/// 获取用户已领取的优惠券
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> UserCoupon([FromBody] FMCouponForUserCouponPost entity)
{
var jm = await _couponServices.GetMyCoupon(_user.ID, 0, entity.display, entity.page, entity.limit);
return jm;
}
#endregion
#region ==================================================
/// <summary>
/// 用户领取优惠券
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetCoupon([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
if (entity.id == 0)
{
jm.msg = GlobalErrorCodeVars.Code15006;
return jm;
}
try
{
_unionOfWork.BeginTran();
//判断优惠券是否可以领取?
var promotionModel = await _promotionServices.ReceiveCoupon(entity.id);
if (promotionModel.status == false)
{
_unionOfWork.RollbackTran();
return promotionModel;
}
var promotion = (CoreCmsPromotion)promotionModel.data;
if (promotion == null)
{
_unionOfWork.RollbackTran();
jm.msg = GlobalErrorCodeVars.Code15019;
return jm;
}
if (promotion.maxNums > 0)
{
//判断用户是否已领取?领取次数
var couponResult = await _couponServices.GetMyCoupon(_user.ID, entity.id, "all", 1, 9999);
if (couponResult.status && couponResult.code >= promotion.maxNums)
{
_unionOfWork.RollbackTran();
jm.msg = GlobalErrorCodeVars.Code15018;
return jm;
}
}
jm = await _couponServices.AddData(_user.ID, entity.id, promotion);
_unionOfWork.CommitTran();
jm.otherData = promotionModel;
}
catch (Exception e)
{
_unionOfWork.RollbackTran();
jm.msg = GlobalErrorCodeVars.Code10000;
}
return jm;
}
#endregion
#region code领取优惠券==================================================
/// <summary>
/// 用户输入code领取优惠券
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetCouponKey([FromBody] FMCouponForGetCouponKeyPost entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.key))
{
jm.msg = GlobalErrorCodeVars.Code15006;
return jm;
}
var coupon = await _couponServices.QueryByClauseAsync(p => p.couponCode == entity.key);
if (coupon == null || coupon.promotionId <= 0)
{
jm.msg = GlobalErrorCodeVars.Code15009;
return jm;
}
//判断优惠券是否可以领取?
var promotionModel = await _promotionServices.ReceiveCoupon(coupon.promotionId);
if (promotionModel.status == false)
{
return promotionModel;
}
//判断用户是否已领取?
if (promotionModel.data is CoreCmsPromotion { maxNums: > 0 } info)
{
//判断用户是否已领取?领取次数
var couponResult = await _couponServices.GetMyCoupon(_user.ID, coupon.promotionId, "all", 1, 9999);
if (couponResult.status && couponResult.code > info.maxNums)
{
jm.msg = GlobalErrorCodeVars.Code15018;
return jm;
}
}
//
jm = await _couponServices.ReceiveCoupon(_user.ID, entity.key);
return jm;
}
#endregion
}
}

View File

@ -1,29 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using Microsoft.AspNetCore.Mvc;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 默认接口示例
/// </summary>
public class DemoController : ControllerBase
{
/// <summary>
/// 默认首页
/// </summary>
/// <returns></returns>
public IActionResult Index()
{
return Content("已结束");
}
}
}

View File

@ -1,313 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 分销请求接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class DistributionController : ControllerBase
{
private readonly ICoreCmsDistributionOrderServices _distributionOrderServices;
private readonly ICoreCmsDistributionServices _distributionServices;
private readonly ICoreCmsSettingServices _settingServices;
private readonly ICoreCmsUserServices _userServices;
private readonly IHttpContextUser _user;
/// <summary>
/// 构造函数
/// </summary>
public DistributionController(IHttpContextUser user, ICoreCmsDistributionServices distributionServices,
ICoreCmsSettingServices settingServices, ICoreCmsUserServices userServices,
ICoreCmsDistributionOrderServices distributionOrderServices)
{
_user = user;
_distributionServices = distributionServices;
_settingServices = settingServices;
_userServices = userServices;
_distributionOrderServices = distributionOrderServices;
}
//公共接口====================================================================================================
#region
/// <summary>
/// 获取店铺信息
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetStoreInfo([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
if (entity.id == 0)
{
jm.msg = "店铺信息丢失";
return jm;
}
var store = UserHelper.GetUserIdByShareCode(entity.id);
if (store <= 0)
{
jm.msg = "店铺信息丢失";
return jm;
}
jm = await _distributionServices.GetStore(store);
return jm;
}
#endregion
//验证接口====================================================================================================
#region
/// <summary>
/// 查询用户是否可以成为分销商
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> Info()
{
var jm = await _distributionServices.GetInfo(_user.ID, true);
return jm;
}
#endregion
#region
/// <summary>
/// 申请成为分销商接口
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> ApplyDistribution([FromBody] FMDistributionApply entity)
{
var jm = new WebApiCallBack();
if (entity.agreement != "on")
{
jm.msg = "请勾选分销协议";
return jm;
}
var iData = new CoreCmsDistribution();
iData.mobile = entity.mobile;
iData.name = entity.name;
iData.weixin = entity.weixin;
iData.qq = entity.qq;
jm = await _distributionServices.AddData(iData, _user.ID);
return jm;
}
#endregion
#region 广
/// <summary>
/// 我推广的订单
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> MyOrder([FromBody] FMPageByIntId entity)
{
var jm = await _distributionServices.GetMyOrderList(_user.ID, entity.page, entity.limit, entity.id);
return jm;
}
#endregion
#region
/// <summary>
/// 店铺设置
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> SetStore([FromBody] FMSetDistributionStorePost entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.storeName))
{
jm.msg = "请填写店铺名称";
return jm;
}
if (string.IsNullOrEmpty(entity.storeLogo))
{
jm.msg = "请上传店铺logo";
return jm;
}
if (string.IsNullOrEmpty(entity.storeBanner))
{
jm.msg = "请上传店铺banner";
return jm;
}
var info = await _distributionServices.QueryByClauseAsync(p => p.userId == _user.ID);
if (info != null)
{
info.storeLogo = entity.storeLogo;
info.storeBanner = entity.storeBanner;
info.storeDesc = entity.storeDesc;
info.storeName = entity.storeName;
await _distributionServices.UpdateAsync(info);
}
jm.status = true;
jm.msg = "保存成功";
return jm;
}
#endregion
#region
/// <summary>
/// 获取我的订单统计
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetOrderSum()
{
var jm = new WebApiCallBack();
//全部订单
var allOrder = await _distributionOrderServices.QueryChildOrderCountAsync(_user.ID, 0);
//一级订单
var firstOrder = await _distributionOrderServices.QueryChildOrderCountAsync(_user.ID);
//二级订单
var secondOrder = await _distributionOrderServices.QueryChildOrderCountAsync(_user.ID, 2);
//本月订单
var monthOrder = await _distributionOrderServices.QueryChildOrderCountAsync(_user.ID, 0, true);
//全部订单金额
var allOrderMoney = await _distributionOrderServices.QueryChildOrderMoneySumAsync(_user.ID, 0);
//代购订单金额
var firstOrderMoney = await _distributionOrderServices.QueryChildOrderMoneySumAsync(_user.ID);
//推广订单金额
var secondOrderMoney = await _distributionOrderServices.QueryChildOrderMoneySumAsync(_user.ID, 2);
//本月订单金额
var monthOrderMoney = await _distributionOrderServices.QueryChildOrderMoneySumAsync(_user.ID, 0, true);
jm.status = true;
jm.data = new
{
allOrder,
firstOrder,
secondOrder,
monthOrder,
allOrderMoney,
firstOrderMoney,
secondOrderMoney,
monthOrderMoney
};
return jm;
}
#endregion
#region
/// <summary>
/// 获取我的下级用户数量
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetTeamSum()
{
var jm = new WebApiCallBack();
//一级统计人数
var first = await _userServices.QueryChildCountAsync(_user.ID);
//二级发展人数
var second = await _userServices.QueryChildCountAsync(_user.ID, 2);
//当月发展一级人数
var monthFirst = await _userServices.QueryChildCountAsync(_user.ID, 1, true);
//当月发展二级分数
var monthSecond = await _userServices.QueryChildCountAsync(_user.ID, 2, true);
jm.status = true;
jm.data = new
{
count = first + second,
first,
second,
monthCount = monthFirst + monthSecond,
monthFirst,
monthSecond
};
return jm;
}
#endregion
#region
/// <summary>
/// 获取分销商排行
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetDistributionRanking([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
var list = await _distributionServices.QueryRankingPageAsync(entity.page, entity.limit);
jm.status = true;
jm.data = new
{
data = list,
list.HasNextPage,
list.HasPreviousPage,
list.PageIndex,
list.PageSize,
list.TotalPages,
list.TotalCount
};
return jm;
}
#endregion
}
}

View File

@ -1,79 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 表单接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class FormController : ControllerBase
{
private readonly ICoreCmsFormServices _formServices;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="formServices"></param>
public FormController(ICoreCmsFormServices formServices)
{
_formServices = formServices;
}
#region /=============================================================================
/// <summary>
/// 万能表单/获取活动商品详情
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetFormDetial([FromBody] FmGetForm entity)
{
var jm = await _formServices.GetFormInfo(entity.id, entity.token);
return jm;
}
#endregion
#region /=============================================================================
/// <summary>
/// 万能表单/提交表单
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> AddSubmit([FromBody] FmAddSubmit entity)
{
var jm = await _formServices.AddSubmit(entity);
jm.otherData = entity;
return jm;
}
#endregion
}
}

View File

@ -1,492 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using AutoMapper;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.Entities.Expression;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Model.ViewModels.DTO;
using CoreCms.Net.Utility.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using SqlSugar;
using CoreCms.Net.Utility.Helper;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 商品相关接口处理
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class GoodController : ControllerBase
{
private IMapper _mapper;
private readonly IHttpContextUser _user;
private ICoreCmsSettingServices _settingServices;
private ICoreCmsGoodsCategoryServices _goodsCategoryServices;
private ICoreCmsGoodsServices _goodsServices;
private ICoreCmsProductsServices _productsServices;
private ICoreCmsBrandServices _brandServices;
private ICoreCmsOrderItemServices _orderItemServices;
private ICoreCmsGoodsCommentServices _goodsCommentServices;
private ICoreCmsGoodsParamsServices _goodsParamsServices;
private ICoreCmsGoodsCollectionServices _goodsCollectionServices;
private ICoreCmsUserServices _userServices;
private ICoreCmsGoodsCategoryExtendServices _goodsCategoryExtendServices;
/// <summary>
/// 构造函数
/// </summary>
public GoodController(IMapper mapper
, IHttpContextUser user
, ICoreCmsSettingServices settingServices
, ICoreCmsGoodsCategoryServices goodsCategoryServices
, ICoreCmsGoodsServices goodsServices
, ICoreCmsProductsServices productsServices
, ICoreCmsBrandServices brandServices
, ICoreCmsOrderItemServices orderItemServices
, ICoreCmsGoodsCommentServices goodsCommentServices
, ICoreCmsGoodsParamsServices goodsParamsServices
, ICoreCmsGoodsCollectionServices goodsCollectionServices
, ICoreCmsUserServices userServices, ICoreCmsGoodsCategoryExtendServices goodsCategoryExtendServices)
{
_mapper = mapper;
_user = user;
_settingServices = settingServices;
_goodsCategoryServices = goodsCategoryServices;
_goodsServices = goodsServices;
_productsServices = productsServices;
_brandServices = brandServices;
_orderItemServices = orderItemServices;
_goodsCommentServices = goodsCommentServices;
_goodsParamsServices = goodsParamsServices;
_goodsCollectionServices = goodsCollectionServices;
_userServices = userServices;
_goodsCategoryExtendServices = goodsCategoryExtendServices;
}
//公共接口====================================================================================================
#region
/// <summary>
/// 获取所有商品分类栏目数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetAllCategories()
{
var jm = new WebApiCallBack() { status = true };
var data = await _goodsCategoryServices.QueryListByClauseAsync(p => p.isShow == true, p => p.sort,
OrderByType.Asc);
var wxGoodCategoryDto = new List<WxGoodCategoryDto>();
var parents = data.Where(p => p.parentId == 0).ToList();
if (parents.Any())
{
parents.ForEach(p =>
{
var model = new WxGoodCategoryDto();
model.id = p.id;
model.name = p.name;
model.imageUrl = !string.IsNullOrEmpty(p.imageUrl) ? p.imageUrl : "/static/images/common/empty.png";
model.sort = p.sort;
var childs = data.Where(p => p.parentId == model.id).ToList();
if (childs.Any())
{
var childsList = new List<WxGoodCategoryChild>();
childs.ForEach(o =>
{
childsList.Add(new WxGoodCategoryChild()
{
id = o.id,
imageUrl = !string.IsNullOrEmpty(o.imageUrl) ? o.imageUrl : "/static/images/common/empty.png",
name = o.name,
sort = o.sort
});
});
model.child = childsList;
}
wxGoodCategoryDto.Add(model);
});
}
jm.status = true;
jm.data = wxGoodCategoryDto;
return jm;
}
#endregion
#region ============================================================
/// <summary>
/// 根据查询条件获取分页数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsPageList([FromBody] FMPageByWhereOrder entity)
{
var jm = new WebApiCallBack();
var where = PredicateBuilder.True<CoreCmsGoods>();
where = where.And(p => p.isDel == false);
where = where.And(p => p.isMarketable == true);
var className = string.Empty;
if (!string.IsNullOrWhiteSpace(entity.where))
{
var obj = JsonConvert.DeserializeAnonymousType(entity.where, new
{
priceFrom = "",
priceTo = "",
catId = "",
brandId = "",
labelId = "",
searchName = "",
});
if (!string.IsNullOrWhiteSpace(obj.priceFrom))
{
var priceF = obj.priceFrom.ObjectToDouble(0);
if (priceF >= 0)
{
var f = Convert.ToDecimal(priceF);
where = where.And(p => p.price >= f);
}
}
if (!string.IsNullOrWhiteSpace(obj.priceTo))
{
var priceT = obj.priceTo.ObjectToDouble(0);
if (priceT > 0)
{
var f = Convert.ToDecimal(priceT);
where = where.And(p => p.price <= f);
}
}
if (!string.IsNullOrWhiteSpace(obj.catId))
{
var catId = obj.catId.ObjectToInt(0);
if (catId > 0)
{
var category = await _goodsCategoryServices.QueryByIdAsync(catId, true);
if (category != null)
{
className = category.name;
}
var categories = await _goodsCategoryServices.QueryAsync(true);
var ids = GoodsHelper.GetChildIds(categories, catId);
//扩展分类
var extends = await _goodsCategoryExtendServices.QueryListByClauseAsync(p => p.goodsCategroyId == catId);
if (extends.Any())
{
var extGoodIds = extends.Select(p => p.goodsId).ToList();
where = where.And(p => ids.Contains(p.goodsCategoryId) || extGoodIds.Contains(p.id));
}
else
{
where = where.And(p => ids.Contains(p.goodsCategoryId));
}
}
}
if (!string.IsNullOrWhiteSpace(obj.brandId))
{
var brandId = obj.brandId.ObjectToInt(0);
if (brandId > 0)
{
where = where.And(p => p.brandId == brandId);
}
}
if (!string.IsNullOrWhiteSpace(obj.labelId))
{
where = where.And(p => (',' + p.labelIds.Trim(',') + ',').Contains(',' + obj.labelId.Trim(',') + ','));
}
if (!string.IsNullOrWhiteSpace(obj.searchName))
{
where = where.And(p => p.name.Contains(obj.searchName));
}
}
//获取数据
var list = await _goodsServices.QueryPageForLinqAsync(where, entity.order, entity.page, entity.limit, false);
if (list.Any())
{
foreach (var goods in list)
{
goods.images = !string.IsNullOrEmpty(goods.images) ? goods.images.Split(",")[0] : "/static/images/common/empty.png";
}
}
//获取品牌
var brands = await _brandServices.QueryListByClauseAsync(p => p.isShow == true, p => p.sort, OrderByType.Desc);
//返回数据
jm.status = true;
jm.data = new
{
list,
className,
entity.page,
list.TotalCount,
list.TotalPages,
entity.limit,
entity.where,
entity.order,
brands
};
jm.msg = "数据调用成功!";
return jm;
}
#endregion
#region ======================================================================
/// <summary>
/// 获取商品详情
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetDetial([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
var userId = 0;
if (_user != null)
{
userId = _user.ID;
}
var model = await _goodsServices.GetGoodsDetial(entity.id, userId, false);
if (model == null)
{
jm.msg = "商品获取失败";
return jm;
}
jm.status = true;
jm.msg = "获取商品详情成功";
jm.data = model;
jm.methodDescription = JsonConvert.SerializeObject(_user);
return jm;
}
#endregion
#region ======================================================================
/// <summary>
/// 获取单个货品信息
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetProductInfo([FromBody] FMGetProductInfo entity)
{
var jm = new WebApiCallBack();
var userId = 0;
if (_user != null)
{
userId = _user.ID;
}
bool bl = entity.type == "pinTuan" || entity.type == "group";
var getProductInfo = await _productsServices.GetProductInfo(entity.id, bl, userId, entity.type, entity.groupId);
if (getProductInfo == null)
{
jm.msg = "获取单个货品失败";
return jm;
}
jm.status = true;
jm.msg = "获取单个货品成功";
jm.data = getProductInfo;
return jm;
}
#endregion
#region ======================================================================
/// <summary>
/// 获取商品评价列表分页数据
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsComment([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
//获取数据
var list = await _goodsCommentServices.QueryPageAsync(p => p.goodsId == entity.id && p.isDisplay == true, p => p.createTime, OrderByType.Desc, entity.page, entity.limit);
if (list.Any())
{
foreach (var item in list)
{
item.imagesArr = !string.IsNullOrEmpty(item.images) ? item.images.Split(",") : null;
}
}
jm.status = true;
jm.msg = "获取评论成功";
jm.data = new
{
list,
commentsCount = list.TotalCount,
totalPages = list.TotalPages
};
return jm;
}
#endregion
#region ======================================================================
/// <summary>
/// 获取单个商品参数
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsParams([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
//获取数据
var goods = await _goodsServices.QueryByIdAsync(entity.id);
if (goods == null)
{
jm.msg = GlobalConstVars.DataisNo;
return jm;
}
var list = new List<WxNameValueDto>();
var goodsParams = await _goodsParamsServices.QueryAsync();
if (!string.IsNullOrEmpty(goods.parameters))
{
var arrItem = goods.parameters.Split("|");
foreach (var item in arrItem)
{
if (!item.Contains(":")) continue;
var childArr = item.Split(":");
if (childArr.Length == 2)
{
var paramsId = Convert.ToInt32(childArr[0]);
var paramsModel = goodsParams.First(p => p.id == paramsId);
if (paramsModel != null)
{
list.Add(new WxNameValueDto()
{
name = paramsModel.name,
value = childArr[1]
});
}
}
}
}
jm.status = true;
jm.msg = "获取商品参数成功";
jm.data = list;
return jm;
}
#endregion
#region ==================================================
/// <summary>
/// 获取随机推荐商品
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsRecommendList([FromBody] FMIntId entity)
{
if (entity.id <= 0)
{
entity.id = 10;
}
var bl = entity.data.ObjectToBool();
var jm = new WebApiCallBack()
{
status = true,
code = 0,
msg = "获取成功",
data = await _goodsServices.GetGoodsRecommendList(entity.id, bl)
};
return jm;
}
#endregion
//验证接口====================================================================================================
#region Token获取商品详情======================================================================
/// <summary>
/// 根据Token获取商品详情
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetDetialByToken([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
var userId = 0;
if (_user != null)
{
userId = _user.ID;
}
var model = await _goodsServices.GetGoodsDetial(entity.id, userId, false);
if (model == null)
{
jm.msg = "商品获取失败";
return jm;
}
await _goodsServices.UpdateAsync(p => new CoreCmsGoods() { viewCount = p.viewCount + 1 },
p => p.id == entity.id);
jm.status = true;
jm.msg = "获取商品详情成功";
jm.data = model;
jm.methodDescription = JsonConvert.SerializeObject(_user);
return jm;
}
#endregion
}
}

View File

@ -1,84 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 团购调用接口数据
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class GroupController : ControllerBase
{
private readonly IHttpContextUser _user;
private readonly ICoreCmsPromotionServices _coreCmsPromotionServices;
private ICoreCmsGoodsServices _goodsServices;
/// <summary>
/// 构造函数
/// </summary>
public GroupController(IHttpContextUser user, ICoreCmsPromotionServices coreCmsPromotionServices, ICoreCmsGoodsServices goodsServices)
{
_user = user;
_coreCmsPromotionServices = coreCmsPromotionServices;
_goodsServices = goodsServices;
}
//公共接口====================================================================================================
#region ===========================================================
/// <summary>
/// 获取秒杀团购列表
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetList([FromBody] FMGroupGetListPost entity)
{
var jm = await _coreCmsPromotionServices.GetGroupList(entity.type, _user.ID, entity.status, entity.page, entity.limit);
return jm;
}
#endregion
#region ===========================================================
/// <summary>
/// 获取秒杀团购详情
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsDetial([FromBody] FMGetGoodsDetial entity)
{
var jm = await _coreCmsPromotionServices.GetGroupDetail(entity.id, 0, "group", entity.groupId);
return jm;
}
#endregion
//验证接口====================================================================================================
}
}

View File

@ -1,162 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Model.ViewModels.DTO;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 页面接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class PageController : ControllerBase
{
private IMapper _mapper;
private readonly ICoreCmsSettingServices _settingServices;
private readonly ICoreCmsPagesServices _pagesServices;
private readonly ICoreCmsOrderServices _orderServices;
private readonly ICoreCmsUserServices _userServices;
/// <summary>
/// 构造函数
/// </summary>
public PageController(IMapper mapper
, ICoreCmsSettingServices settingServices
, ICoreCmsPagesServices pagesServices
, ICoreCmsOrderServices orderServices
, ICoreCmsUserServices userServices)
{
_mapper = mapper;
_settingServices = settingServices;
_pagesServices = pagesServices;
_orderServices = orderServices;
_userServices = userServices;
}
//公共接口====================================================================================================
#region =============================================================
/// <summary>
/// 获取页面布局数据
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
[HttpPost]
[Description("获取页面布局数据")]
public async Task<WebApiCallBack> GetPageConfig([FromBody] FMWxPost entity)
{
var jm = await _pagesServices.GetPageConfig(entity.code);
return jm;
}
#endregion
#region =============================================================
/// <summary>
/// 获取用户购买记录
/// </summary>
[HttpPost]
[Description("获取用户购买记录")]
public async Task<WebApiCallBack> GetRecod([FromBody] FMGetRecodPost entity)
{
var jm = new WebApiCallBack() { status = true, msg = "获取成功", otherData = entity };
/***
*
*
*/
//logo作为头像
Random rand = new Random();
var allConfigs = await _settingServices.GetConfigDictionaries();
var avatar = CommonHelper.GetConfigDictionary(allConfigs, SystemSettingConstVars.ShopLogo);
var names = new string[] { "无人像你", "啭裑①羣豞", "朕射妳无罪", "骑着蜗牛狂奔", "残孤星", "上网可以别开QVOD", "请把QQ留下", "蹭网可以,一小时两块钱", "I在。哭泣", "不倾国倾城只倾他一人", "你再发光我就拔你插头", "家,世间最温暖的地方", "挥着鸡翅膀的女孩", "难不难过都是一个人过", "原谅我盛装出席只为错过你", "残孤星", "只适合被遗忘", "爱情,算个屁丶", "执子辶掱", "朕今晚翻你牌子", "①苆兜媞命", "中华一样的高傲", "始于心动止于枯骨", "我们幸福呢", "表白失败,勿扰", "髮型吥能亂", "陽咣丅啲憂喐", "你棺材是翻盖的还是滑盖的", "孤枕", "泪颜葬相思", "喵星人", "超拽霸气的微博名字", "晚安晚安晚晚难安", "却输给了秒", "为什么我吃德芙没有黑丝飘", "请输入我大" };
var listUsers = new List<RandUser>();
foreach (var itemName in names)
{
var min = rand.Next(100, 1000);
var createTime = DateTime.Now.AddMinutes(-min);
listUsers.Add(new RandUser()
{
avatar = avatar,
createTime = CommonHelper.TimeAgo(createTime),
nickname = itemName,
desc = "下单成功",
dt = createTime
});
}
if (entity.type == "home")
{
//数据库里面随机取出来几条数据
var orders = await _orderServices.QueryListByClauseAsync(p => p.isdel == false, 20, p => p.createTime,
OrderByType.Desc);
if (orders != null && orders.Any())
{
Random rd = new Random();
var index = rd.Next(orders.Count);
var orderItem = orders[index];
if (orderItem != null)
{
var user = await _userServices.QueryByIdAsync(orderItem.userId);
if (user != null && !string.IsNullOrEmpty(user.nickName))
{
jm.data = new RandUser()
{
avatar = !string.IsNullOrEmpty(user.avatarImage) ? user.avatarImage : avatar,
createTime = CommonHelper.TimeAgo(orderItem.createTime),
nickname = user.nickName,
desc = "下单成功",
dt = orderItem.createTime
};
}
}
}
else
{
Random rd = new Random();
var listI = rd.Next(listUsers.Count);
jm.data = listUsers[listI];
}
}
return jm;
}
#endregion
//验证接口====================================================================================================
}
}

View File

@ -1,175 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Utility.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 拼团接口
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class PinTuanController : ControllerBase
{
private readonly IHttpContextUser _user;
private readonly ICoreCmsPinTuanGoodsServices _pinTuanGoodsServices;
private readonly ICoreCmsPinTuanRuleServices _pinTuanRuleServices;
private readonly ICoreCmsProductsServices _productsServices;
private readonly ICoreCmsPinTuanRecordServices _pinTuanRecordServices;
private readonly ICoreCmsGoodsServices _goodsServices;
/// <summary>
/// 构造函数
/// </summary>
public PinTuanController(IHttpContextUser user
, ICoreCmsPinTuanGoodsServices pinTuanGoodsServices
, ICoreCmsPinTuanRuleServices pinTuanRuleServices
, ICoreCmsProductsServices productsServices
, ICoreCmsPinTuanRecordServices pinTuanRecordServices, ICoreCmsGoodsServices goodsServices)
{
_user = user;
_pinTuanGoodsServices = pinTuanGoodsServices;
_pinTuanRuleServices = pinTuanRuleServices;
_productsServices = productsServices;
_pinTuanRecordServices = pinTuanRecordServices;
_goodsServices = goodsServices;
}
#region
/// <summary>
/// 拼团列表
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetList([FromBody] FMIntId entity)
{
WebApiCallBack jm;
var userId = 0;
if (_user != null)
{
userId = _user.ID;
}
var id = 0;
if (entity.id > 0)
{
id = entity.id;
}
jm = await _pinTuanRuleServices.GetPinTuanList(id, userId);
return jm;
}
#endregion
#region
/// <summary>
/// 获取拼团商品信息
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetGoodsInfo([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
var userId = 0;
if (_user != null)
{
userId = _user.ID;
}
var pinTuanStatus = entity.data.ObjectToInt(1);
jm.status = true;
jm.msg = "获取详情成功";
jm.data = await _pinTuanGoodsServices.GetGoodsInfo(entity.id, userId, pinTuanStatus);
return jm;
}
#endregion
#region
/// <summary>
/// 获取货品信息
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetProductInfo([FromBody] FMGetProductInfo entity)
{
var jm = new WebApiCallBack();
var products = await _productsServices.GetProductInfo(entity.id, false, 0, entity.type);
if (products == null)
{
jm.msg = GlobalErrorCodeVars.Code10000;
return jm;
}
//把拼团的一些属性等加上
var info = await _pinTuanRuleServices.QueryMuchFirstAsync<CoreCmsPinTuanRule, CoreCmsPinTuanGoods, CoreCmsPinTuanRule>(
(join1, join2) => new object[] { JoinType.Left, join1.id == join2.ruleId },
(join1, join2) => join1, (join1, join2) => join2.goodsId == products.goodsId);
if (info == null)
{
jm.msg = GlobalErrorCodeVars.Code10000;
return jm;
}
products.pinTuanRule = info;
jm.status = true;
jm.data = products;
return jm;
}
#endregion
#region id取拼团信息
/// <summary>
/// 根据订单id取拼团信息用在订单详情页
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetPinTuanTeam([FromBody] FMGetPinTuanTeamPost entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.orderId) && entity.teamId == 0)
{
jm.msg = GlobalErrorCodeVars.Code15606;
return jm;
}
jm = await _pinTuanRecordServices.GetTeamList(entity.teamId, entity.orderId);
return jm;
}
#endregion
}
}

View File

@ -1,404 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.Entities.Expression;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Utility.Extensions;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 服务卡控制器
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class ServiceController : ControllerBase
{
private readonly ICoreCmsServicesServices _servicesServices;
private readonly ICoreCmsUserServicesOrderServices _userServicesOrderServices;
private readonly ICoreCmsUserServicesTicketServices _userServicesTicketServices;
private readonly ICoreCmsUserServices _userServices;
private readonly ICoreCmsUserServicesTicketVerificationLogServices _ticketVerificationLogServices;
private readonly ICoreCmsClerkServices _clerkServices;
private readonly ICoreCmsStoreServices _storeServices;
private readonly ICoreCmsUserGradeServices _userGradeServices;
private readonly IHttpContextUser _user;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="servicesServices"></param>
/// <param name="user"></param>
/// <param name="userServicesOrderServices"></param>
/// <param name="userServicesTicketServices"></param>
/// <param name="userServices"></param>
/// <param name="clerkServices"></param>
/// <param name="ticketVerificationLogServices"></param>
/// <param name="storeServices"></param>
/// <param name="userGradeServices"></param>
public ServiceController(ICoreCmsServicesServices servicesServices, IHttpContextUser user, ICoreCmsUserServicesOrderServices userServicesOrderServices, ICoreCmsUserServicesTicketServices userServicesTicketServices, ICoreCmsUserServices userServices, ICoreCmsClerkServices clerkServices, ICoreCmsUserServicesTicketVerificationLogServices ticketVerificationLogServices, ICoreCmsStoreServices storeServices, ICoreCmsUserGradeServices userGradeServices)
{
_servicesServices = servicesServices;
_user = user;
_userServicesOrderServices = userServicesOrderServices;
_userServicesTicketServices = userServicesTicketServices;
_userServices = userServices;
_clerkServices = clerkServices;
_ticketVerificationLogServices = ticketVerificationLogServices;
_storeServices = storeServices;
_userGradeServices = userGradeServices;
}
#region
/// <summary>
/// 取得服务卡列表信息
/// </summary>
/// <returns></returns>
[HttpPost]
//[Authorize]
public async Task<WebApiCallBack> GetPageList([FromBody] FMPageByIntId entity)
{
var jm = new WebApiCallBack();
var dt = DateTime.Now;
var where = PredicateBuilder.True<CoreCmsServices>();
where = where.And(p => p.status == (int)GlobalEnumVars.ServicesStatus.Shelve);
where = where.And(p => p.amount > 0);
where = where.And(p => p.startTime < dt && p.endTime > dt);
var list = await _servicesServices.QueryPageAsync(where, p => p.createTime, OrderByType.Desc, entity.page, entity.limit);
if (list.Any())
{
var storesAll = await _storeServices.QueryAsync();
var userGradesAll = await _userGradeServices.QueryAsync();
foreach (var data in list)
{
TimeSpan ts = data.endTime.Subtract(dt);
data.timestamp = (int)ts.TotalSeconds;
if (!string.IsNullOrEmpty(data.consumableStore))
{
var consumableStoreStr = CommonHelper.GetCaptureInterceptedText(data.consumableStore, ",");
var consumableStoreIds = CommonHelper.StringToIntArray(consumableStoreStr);
if (consumableStoreIds.Any())
{
var stores = storesAll.Where(p => consumableStoreIds.Contains(p.id)).ToList();
data.consumableStores = stores.Select(p => p.storeName).ToList();
}
}
if (!string.IsNullOrEmpty(data.allowedMembership))
{
var allowedMembershipStr = CommonHelper.GetCaptureInterceptedText(data.allowedMembership, ",");
var allowedMembershipIds = CommonHelper.StringToIntArray(allowedMembershipStr);
if (allowedMembershipIds.Any())
{
var userGrades = userGradesAll.Where(p => allowedMembershipIds.Contains(p.id)).ToList();
data.allowedMemberships = userGrades.Select(p => p.title).ToList();
}
}
}
}
jm.status = true;
jm.data = new
{
list = list,
count = list.TotalCount,
};
return jm;
}
#endregion
#region
/// <summary>
/// 获取服务卡详情
/// </summary>
/// <returns></returns>
[HttpPost]
//[Authorize]
public async Task<WebApiCallBack> GetDetails([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
var data = await _servicesServices.QueryByClauseAsync(p => p.id == entity.id);
if (data != null)
{
var dt = DateTime.Now;
TimeSpan ts = data.endTime.Subtract(dt);
data.timestamp = (int)ts.TotalSeconds;
if (!string.IsNullOrEmpty(data.consumableStore))
{
var consumableStoreStr = CommonHelper.GetCaptureInterceptedText(data.consumableStore, ",");
var consumableStoreIds = CommonHelper.StringToIntArray(consumableStoreStr);
if (consumableStoreIds.Any())
{
var stores = await _storeServices.QueryListByClauseAsync(p => consumableStoreIds.Contains(p.id));
data.consumableStores = stores.Select(p => p.storeName).ToList();
}
}
if (!string.IsNullOrEmpty(data.allowedMembership))
{
var allowedMembershipStr = CommonHelper.GetCaptureInterceptedText(data.allowedMembership, ",");
var allowedMembershipIds = CommonHelper.StringToIntArray(allowedMembershipStr);
if (allowedMembershipIds.Any())
{
var userGrades = await _userGradeServices.QueryListByClauseAsync(p => allowedMembershipIds.Contains(p.id));
data.allowedMemberships = userGrades.Select(p => p.title).ToList();
}
}
}
jm.status = true;
jm.data = data;
return jm;
}
#endregion
//验证接口====================================================================================================
#region
/// <summary>
/// 取得服务卡列表信息
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> AddServiceOrder([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack();
var data = await _servicesServices.QueryByClauseAsync(p => p.id == entity.id);
if (data == null)
{
jm.msg = "服务数据获取失败";
return jm;
}
var user = await _userServices.QueryByIdAsync(_user.ID);
if (user == null)
{
jm.msg = "用户数据获取失败";
return jm;
}
if (!data.allowedMembership.Contains("," + user.grade + ","))
{
jm.msg = "您所在的用户级别不支持购买";
return jm;
}
var order = new CoreCmsUserServicesOrder();
order.serviceOrderId = CommonHelper.GetSerialNumberType((int)GlobalEnumVars.SerialNumberType.);
order.userId = _user.ID;
order.servicesId = entity.id;
order.isPay = false;
order.status = (int)GlobalEnumVars.ServicesOrderStatus.;
order.createTime = DateTime.Now;
var bl = await _userServicesOrderServices.InsertAsync(order) > 0;
jm.status = bl;
jm.data = order.serviceOrderId;
return jm;
}
#endregion
#region
/// <summary>
/// 店铺核销的服务券列表
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> VerificationPageList([FromBody] FMPageByIntId entity)
{
var jm = await _ticketVerificationLogServices.GetVerificationLogs(_user.ID, entity.page, entity.limit);
return jm;
}
#endregion
#region
/// <summary>
/// 软删除服务券核销单数据
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> LogDelete([FromBody] FMIntId entity)
{
var jm = await _ticketVerificationLogServices.LogDelete(entity.id, _user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 获取单个提货单详情
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetTicketInfo([FromBody] FMStringId entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.id))
{
jm.msg = "请提交查询数据关键词";
return jm;
}
var ticket = await _userServicesTicketServices.QueryByClauseAsync(p => p.redeemCode == entity.id);
if (ticket == null)
{
jm.msg = "未查询到服务券";
return jm;
}
ticket.statusStr = EnumHelper.GetEnumDescriptionByValue<GlobalEnumVars.ServicesTicketStatus>(ticket.status);
var service = await _servicesServices.QueryByClauseAsync(p => p.id == ticket.serviceId);
var serviceOrder =
await _userServicesOrderServices.QueryByClauseAsync(p => p.serviceOrderId == ticket.serviceOrderId);
jm.status = true;
jm.data = new
{
ticket,
service,
serviceOrder
};
return jm;
}
#endregion
#region
/// <summary>
/// 核销服务券
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> VerificationTicket([FromBody] FMStringId entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.id))
{
jm.msg = "请提交查询数据关键词";
return jm;
}
var ticket = await _userServicesTicketServices.QueryByClauseAsync(p => p.redeemCode == entity.id);
if (ticket == null)
{
jm.msg = "未查询到服务券";
return jm;
}
if (ticket.status != (int)GlobalEnumVars.ServicesTicketStatus.Normal)
{
jm.msg = "服务券状态不支持核销";
return jm;
}
var service = await _servicesServices.QueryByIdAsync(ticket.serviceId);
if (service == null)
{
jm.msg = "服务项目获取失败";
return jm;
}
var user = await _userServices.QueryByIdAsync(_user.ID);
if (user == null)
{
jm.msg = "未获取到审核权限";
return jm;
}
var clerk = await _clerkServices.QueryByClauseAsync(p => p.userId == user.id);
if (clerk == null)
{
jm.msg = "非门店店员无权限核验";
return jm;
}
if (!service.consumableStore.Contains("," + clerk.storeId + ","))
{
jm.msg = "您所在的门店无权核销此券";
return jm;
}
//开始更新数据
var log = new CoreCmsUserServicesTicketVerificationLog
{
storeId = clerk.storeId,
verificationUserId = _user.ID,
ticketId = ticket.id,
ticketRedeemCode = ticket.redeemCode,
verificationTime = DateTime.Now,
serviceId = ticket.serviceId,
isDel = false
};
ticket.status = (int)GlobalEnumVars.ServicesTicketStatus.Verification;
ticket.verificationTime = DateTime.Now;
ticket.isVerification = true;
var up = await _userServicesTicketServices.UpdateAsync(ticket);
var bl = false;
if (up)
{
bl = await _ticketVerificationLogServices.InsertAsync(log) > 0;
}
jm.status = up && bl;
jm.msg = jm.status ? "核销成功" : "核销失败";
return jm;
}
#endregion
}
}

View File

@ -1,365 +0,0 @@
/***********************************************************************
* Project: CoreCms
* ProjectName:
* Web: https://www.corecms.net
* Author:
* Email: jianweie@163.com
* CreateTime: 2021/1/31 21:45:10
* Description:
***********************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreCms.Net.Auth.HttpContextUser;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using CoreCms.Net.Model.Entities.Expression;
using CoreCms.Net.Model.FromBody;
using CoreCms.Net.Model.ViewModels.UI;
using CoreCms.Net.Model.ViewModels.DTO;
using CoreCms.Net.Utility.Extensions;
using CoreCms.Net.Utility.Helper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
namespace CoreCms.Net.Web.WebApi.Controllers
{
/// <summary>
/// 门店调用接口数据
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
public class StoreController : ControllerBase
{
private readonly IHttpContextUser _user;
private readonly ICoreCmsStoreServices _storeServices;
private readonly ICoreCmsClerkServices _clerkServices;
private readonly ICoreCmsSettingServices _settingServices;
private readonly ICoreCmsBillLadingServices _billLadingServices;
private readonly ICoreCmsOrderServices _orderServices;
/// <summary>
/// 构造函数
/// </summary>
public StoreController(IHttpContextUser user
, ICoreCmsStoreServices storeServices
, ICoreCmsClerkServices clerkServices
, ICoreCmsSettingServices settingServices
, ICoreCmsBillLadingServices billLadingServices, ICoreCmsOrderServices orderServices)
{
_user = user;
_storeServices = storeServices;
_clerkServices = clerkServices;
_settingServices = settingServices;
_billLadingServices = billLadingServices;
_orderServices = orderServices;
}
//公共接口======================================================================================================
#region
/// <summary>
/// 获取默认的门店
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetDefaultStore()
{
var jm = new WebApiCallBack();
var ship = await _storeServices.QueryByClauseAsync(p => p.isDefault == true);
jm.status = true;
jm.data = ship;
return jm;
}
#endregion
#region
/// <summary>
/// 获取门店列表数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetStoreList([FromBody] FMGetStoreQueryPageByCoordinate entity)
{
var jm = new WebApiCallBack();
try
{
var where = PredicateBuilder.True<CoreCmsStore>();
if (!string.IsNullOrEmpty(entity.key))
{
where = where.And(p => p.storeName.Contains(entity.key));
}
jm.status = true;
var data = await _storeServices.QueryPageAsyncByCoordinate(where, p => p.distance, OrderByType.Asc, entity.page, entity.limit, entity.latitude, entity.longitude);
foreach (var item in data)
{
if (item.distance > 0)
{
if (item.distance > 1000)
{
item.distanceStr = Math.Round(item.distance / 1000, 2) + "km";
}
else
{
item.distanceStr = Math.Round(item.distance, 2) + "m";
}
}
else
{
item.distanceStr = "未知";
}
}
jm.data = data;
jm.otherData = new
{
totalCount = data.TotalCount,
totalPages = data.TotalPages,
};
}
catch (Exception e)
{
jm.msg = GlobalConstVars.DataHandleEx;
jm.data = e.ToString();
}
return jm;
}
#endregion
#region
/// <summary>
/// 获取推荐关键词
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetRecommendKeys()
{
var jm = new WebApiCallBack();
var allConfigs = await _settingServices.GetConfigDictionaries();
var recommendKeysStr = CommonHelper.GetConfigDictionary(allConfigs, SystemSettingConstVars.RecommendKeys);
jm.status = true;
jm.msg = "获取成功";
jm.data = !string.IsNullOrEmpty(recommendKeysStr) ? recommendKeysStr.Split("|") : new string[] { };
return jm;
}
#endregion
#region
/// <summary>
/// 判断是否开启门店自提
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetStoreSwitch()
{
var jm = new WebApiCallBack { status = true, msg = "获取成功" };
var allConfigs = await _settingServices.GetConfigDictionaries();
jm.data = CommonHelper.GetConfigDictionary(allConfigs, SystemSettingConstVars.StoreSwitch).ObjectToInt(2); ;
return jm;
}
#endregion
#region
/// <summary>
/// 根据序列获取门店数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<WebApiCallBack> GetStoreById([FromBody] FMIntId entity)
{
var jm = new WebApiCallBack
{
status = true,
msg = "获取成功",
data = await _storeServices.QueryByClauseAsync(p => p.id == entity.id)
};
return jm;
}
#endregion
//验证接口======================================================================================================
#region 访
/// <summary>
/// 判断访问用户是否是店员
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> IsClerk()
{
var jm = await _clerkServices.IsClerk(_user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 根据用户序列获取门店数据
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetStoreByUserId()
{
var jm = new WebApiCallBack
{
status = true,
msg = "获取成功",
data = await _storeServices.GetStoreByUserId(_user.ID)
};
return jm;
}
#endregion
#region
/// <summary>
/// 获取个人订单列表
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetOrderPageByMerchant([FromBody] GetOrderPageByMerchantPost entity)
{
var jm = new WebApiCallBack();
var store = await _storeServices.GetStoreByUserId(_user.ID);
if (store != null)
{
jm = await _orderServices.GetOrderPageByMerchant(entity.dateType, entity.date, entity.status, entity.storeId, entity.page, entity.limit);
}
else
{
jm.status = false;
jm.msg = "你不是店员";
}
return jm;
}
#endregion
#region
/// <summary>
/// 搜索订单
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> GetOrderPageByMerchantSearch([FromBody] GetOrderPageByMerchantSearcgPost entity)
{
var jm = new WebApiCallBack();
var store = await _storeServices.GetStoreByUserId(_user.ID);
if (store != null)
{
jm = await _orderServices.GetOrderPageByMerchantSearch(entity.keyword, entity.status, entity.receiptType, entity.storeId, entity.page, entity.limit);
}
else
{
jm.status = false;
jm.msg = "你不是店员";
}
return jm;
}
#endregion
#region
/// <summary>
/// 店铺提货单列表
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> StoreLadingList([FromBody] FMPageByIntId entity)
{
var jm = await _billLadingServices.GetStoreLadingList(_user.ID, entity.page, entity.limit);
return jm;
}
#endregion
#region
/// <summary>
/// 删除提货单数据
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> LadingDelete([FromBody] FMStringId entity)
{
var jm = await _billLadingServices.LadingDelete(entity.id, _user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 获取单个提货单详情
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> LadingInfo([FromBody] FMStringId entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.id))
{
jm.msg = "请提交查询数据关键词";
return jm;
}
jm = await _billLadingServices.GetInfo(entity.id, _user.ID);
return jm;
}
#endregion
#region
/// <summary>
/// 核销订单
/// </summary>
/// <returns></returns>
[HttpPost]
[Authorize]
public async Task<WebApiCallBack> Lading([FromBody] FMStringId entity)
{
var jm = new WebApiCallBack();
if (string.IsNullOrEmpty(entity.id))
{
jm.msg = "请提交查询数据关键词";
return jm;
}
var array = entity.id.Split(",");
var result = await _billLadingServices.LadingOperating(array, _user.ID);
jm.status = result.code == 0;
jm.msg = result.msg;
return jm;
}
#endregion
}
}

@ -1 +1 @@
Subproject commit 316d1e2b5d7013d0b9f7507ac2f15ead5ff4d703
Subproject commit 05ad1348cf7b9ec80590919c3f23c2cb2c077e70

25
v1.0.0_需求文档.md Normal file
View File

@ -0,0 +1,25 @@
一、会员档案功能
1、会员第一次使用需要授权微信号登录绑定联系电话 并填写个人信息
如: 打什么类型的麻将(扑克) 性别 年龄 是否抽烟 常消费包厢类型(大中小包)
2、会员行为标签
设定会员星级及黑名单,主要用于规避恶意组局及恶意逃单人员
星级包括牌品 牌技 形象 初始全为4分
组局结束后可找到历史对局,并对历史牌友进行评价
统计顾客组局成功率 被投诉次数 消费频次 消费能力 鸽子次数并且公开可查看
3、要有相应的后台管理顾客档案
二、麻友匹配机制
1、需求发布
用户可发布组局需求,需填写信息包含时间地点包厢号包厢类型是否禁烟游戏偏好等
地点为单一店名 主要填写包厢号 包厢号需和包厢类型已经包厢费用绑定
2、组局参与
其他客户可查看组局人员信息,时间地点信息等详情 同意即可参与
3、组局成功后自动发送微信消息给参与者消息包含组局的时间地点信息该消息可在后台设置相应模板
4、组局成功后发起组局者局头会收到组局其他参与者的信息其中包含其他人联系方式微信名及一张现金券通知组局者本次消费成功后他将会获得现金奖励或当局立减
5、组局成功后店铺经营人员微信可收到推送消息并查看组局情况以确定核销内容及包间情况
6、鸽子费用定金组局成功可设置相应鸽子费用分为三挡0/5/15发起组局时发起人可选择是否设置鸽子费如设置鸽子费则需线上支付鸽子费至平台账户四人皆到店后鸽子费原路返还若有人爽约爽约人定金作为其他三人补偿不予退还并记录其爽约
7、组局奖励推送
三、广告及推送内容
1、使用软件时会跳出店铺会员注册的二维码弹窗广告用于线下引流
2、免责声明及保证金说明
3、本店铺相应logo及图片广告位

114
v1.0.1_需求文档.md Normal file
View File

@ -0,0 +1,114 @@
v1.1版本更新
需求简介
1. 增加新功能:
1.1. 站内信。
1.2. 预约页优化。
1.3. 我的收益。
一、站内信
(1) 入口
1. 我的页面,功能区,新增站内信入口【我的消息】。
站内信 入口
1.1. 有新消息时icon右上角显示“红点”。
(2) 详情页
1. 显示所有通知,以最近时间为排序。
站内信 详情页
1.1. 显示标题、正文、通知时间。
1.2. 正文过长时,自动延长本条消息的长度。
1.3. 进入本页面时,默认已读所有消息。
(3) 通知发送
1. 在后台可配置通知的发送。
2. 通知分为自动发送、手动发送。
2.1. 自动发送的通知,根据不同的条件提前固定好模板内容。
3. 自动发送:
3.1. 组局成功时,所有参与人收到相应的通知。
3.2. 组局失败时,所有参与人收到相应的通知。
3.3. ……待补充其他自动发送规则。
4. 手动发送:
4.1. 可指定单个、多个用户发送相同的通知,通知内容可自定义。
4.2. 可向全部用户发送相同的通知,通知内容可自定义。
5. 用户只能单方向接收通知,不能对通知进行回复。
二、预约页优化
(1) 新增每天房间空闲/使用展示页
每天房间空闲/使用展示
1. 展示从“今天”起到未来7天内的房间列表。
1.1. 点击日期,切换对应日期的房间预约和使用情况。
2. 时间段规则:
2.1. 凌晨0点 ~ 5点59分。
2.2. 上午6点 ~ 11点59分。
2.3. 下午12点 ~ 17点59分。
2.4. 晚上18点 ~ 23点59分。
3. 【查看当前时段空闲房间】按钮默认为“关闭”状态,点击后变为“开启”状态。开启后只展示当前时段内“可预约”的房间。
4. 房间列表,展示房间图片、房间号/房间名、房间类型、标准价格、会员价格、每天4个时段的可预约状态、当前时段的使用状态、【预约】按钮。
房间信息和当日时段预约状态
4.1. 后台可配置房间图片、房间号/房间名、房间类型、标准价格、会员价格。
4.1.1. 会员,为线下店内的会员,与小程序无关,小程序仅作价格展示。
4.1.2. 标准价格与会员价格在配置时按“xx元/xx小时”为格式进行配置
4.1.2.1. 标准价格30元/4小时。
4.1.2.2. 会员价格30元/5小时 或 20元/4小时。
4.2. 4个时段的可预约状态根据用户预约单、后台操作自动改变。
5. 若今日内,所有时间段都已被约满,【预约】按钮处于“灰色不可点击”状态。
6. 点击【预约】跳转到“预约页”。
7. 后台可设置每个房间号在不同日期、时间段内的预约状态。
7.1. 用于线下预约房间后,将房间状态同步至线上。
(2) 预约页
预约页
1. 预约日期,不可更改,根据上级页面的选项自动展示。
2. 预约时间,可选择“凌晨”“上午”“下午”“晚上”。
3. 最晚到店时间:
3.1. 日期不可选,默认当天。
3.2. 选择时间,不可小于当前时段,不可超出当天。
4. 房间号,不可更改,根据上级页面的选项自动展示。
5. 人数,新增“无需组局”选项。
5.1. 相当于“人数1人”只有发起者自己。
6. 鸽子费,新增“自定义”选项。
鸽子费
6.1. 不能超出50元。
6.2. 只能输入整数。
三、我的收益
(1) 入口
1. 我的页面,新增【我的收益】。
站内信 入口
(2) 详情页
我的收益 详情页
1. 待提现收益,未提现的剩余金额。
2. 已提现收益,所有已提现的金额总和。
3. 点击【查看规则】,弹出“规则说明弹窗”。
弹窗说明
3.1. 内容后台配置。
4. 收益记录,展示每次收益的数据。
收益记录
5. 提现记录,展示每次提现的数据。
提现记录
5.1. 多种状态,“提现中”“已提现”“已取消”。
5.1.1. 提现中:申请后,处于该状态。
5.1.2. 已提现:已提现,线下打款后处于该状态。
5.1.3. 已取消:后台取消/拒绝该提现申请。
(3) 收益如何获取
1. 线下员工通过后台,给发起者的账号添加佣金。
1.1. 只有发起者有佣金,参与者没有。
2. 佣金 = 房费10%。
(4) 提现
1. 点击【去提现】,弹出提现弹窗。
提现弹窗
1.1. 支持小数点后两位。
1.2. 点击【全部提现】,自动输入所有待提现金额。
1.3. 点击【申请提现】:
1.3.1. 高于待提现金额,弹出系统提示“超出可提现金额”。
1.3.2. 无其他问题,关闭弹窗,弹出系统提示“已提交申请”,页面自动刷新,更新待提现金额。
2. 后台展示收到的提现申请记录,可对申请进行操作。
2.1. 同意:同意该申请,线下联系客户打款。
2.2. 拒绝/取消:因其他原因拒绝该申请。
2.3. 已打款:通过线下转帐后,将该记录手动改变为本状态。

1190
业务逻辑文档 copy.md Normal file

File diff suppressed because it is too large Load Diff

1782
业务逻辑文档.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,556 @@
# 预约时间自由选择 - 改动文档
## 1. 需求概述
### 1.1 当前问题
- 用户只能选择固定时段凌晨0-6、上午6-12、下午12-18、晚上18-24
- 无法灵活选择具体的开始和结束时间
- 跨时段预约显示不直观
### 1.2 改动目标
1. **时段显示优化**如果预约时间为15:00-20:00房间列表页应显示该房间"下午"和"晚上"都被预定
2. **自由时间选择**:创建预约界面允许用户自由选择开始时间和结束时间
---
## 2. 改动前分析
### 2.1 当前时段定义
```
时段类型 | 时间范围 | 对应值
--------|----------|-------
凌晨 | 00:00-06:00 | 0
上午 | 06:00-12:00 | 1
下午 | 12:00-18:00 | 2
晚上 | 18:00-24:00 | 3
```
### 2.2 当前预约创建流程
```mermaid
flowchart TD
A[用户选择日期] --> B[选择房间]
B --> C[显示可用时段]
C --> D{选择固定时段}
D -->|凌晨| E1[设置 00:00-06:00]
D -->|上午| E2[设置 06:00-12:00]
D -->|下午| E3[设置 12:00-18:00]
D -->|晚上| E4[设置 18:00-24:00]
E1 --> F[提交预约]
E2 --> F
E3 --> F
E4 --> F
F --> G[后端创建预约]
```
### 2.3 当前前端时间计算逻辑
**文件**: `pages/appointment/appointment-page.vue`
```javascript
// 当前代码:根据时段类型计算固定时间
const calculateTimeFromSlot = () => {
const date = new Date(selectedDate.value);
let startHour, endHour;
switch (selectedTimeSlot.value) {
case 0: // 凌晨
startHour = 0;
endHour = 6;
break;
case 1: // 上午
startHour = 6;
endHour = 12;
break;
case 2: // 下午
startHour = 12;
endHour = 18;
break;
case 3: // 晚上
startHour = 18;
endHour = 24;
break;
}
// ... 设置开始和结束时间
};
```
### 2.4 当前房间时段状态显示
**文件**: `pages/appointment/book-room-page.vue`
```
┌─────────────────────────────────────────┐
│ 房间名称 │
├─────────────────────────────────────────┤
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟢 │
│ │
│ 🟢 可预约 🟠 已预约 ⚫ 不可用 🔵 使用中 │
└─────────────────────────────────────────┘
当前问题预约15:00-20:00时仅显示一个时段被占用
```
### 2.5 当前后端时段判断逻辑
**文件**: `CoreCms.Net.Services/SQ/SQRoomsServices.cs`
```csharp
// 后端已有重叠判断逻辑(可复用)
bool isReserved = reservations?.Any(r =>
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
```
**分析**:后端逻辑已支持跨时段检测,问题在于前端只能选择单一时段。
---
## 3. 改动后方案
### 3.1 新的预约创建流程
```mermaid
flowchart TD
A[用户选择日期] --> B[选择房间]
B --> C[显示房间详情]
C --> D[选择开始时间]
D --> E[选择结束时间]
E --> F{时间校验}
F -->|结束时间 > 开始时间| G[计算跨越时段]
F -->|无效| H[提示错误]
G --> I[检查时段冲突]
I -->|无冲突| J[提交预约]
I -->|有冲突| K[提示时段已被预约]
J --> L[后端创建预约]
L --> M[更新房间时段显示]
```
### 3.2 新的时间选择UI设计
```
改动前(固定时段选择):
┌─────────────────────────────────────────┐
│ 选择时段 │
├─────────────────────────────────────────┤
│ ○ 凌晨 (00:00-06:00) │
│ ○ 上午 (06:00-12:00) │
│ ● 下午 (12:00-18:00) ← 只能单选 │
│ ○ 晚上 (18:00-24:00) │
└─────────────────────────────────────────┘
改动后(自由时间选择):
┌─────────────────────────────────────────┐
│ 选择时间 │
├─────────────────────────────────────────┤
│ 开始时间: [15:00 ▼] ← 时间选择器 │
│ 结束时间: [20:00 ▼] ← 时间选择器 │
│ │
│ 预计时长: 5小时 │
│ 跨越时段: 下午、晚上 │
│ │
│ 💡 提示:结束时间必须晚于开始时间 │
└─────────────────────────────────────────┘
```
### 3.3 新的房间列表时段显示
```
改动前15:00-20:00预约后:
┌─────────────────────────────────────────┐
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟢 ← 只显示下午 │
└─────────────────────────────────────────┘
改动后15:00-20:00预约后:
┌─────────────────────────────────────────┐
│ [凌晨] [上午] [下午] [晚上] │
│ 🟢 🟢 🟠 🟠 ← 下午+晚上 │
└─────────────────────────────────────────┘
```
### 3.4 时段重叠判断逻辑
```mermaid
flowchart LR
subgraph 预约时间 15:00-20:00
P1[15:00] --> P2[20:00]
end
subgraph 时段判断
S1[凌晨 0-6] --> R1{重叠?}
S2[上午 6-12] --> R2{重叠?}
S3[下午 12-18] --> R3{重叠?}
S4[晚上 18-24] --> R4{重叠?}
end
R1 -->|否| N1[🟢]
R2 -->|否| N2[🟢]
R3 -->|是| Y3[🟠]
R4 -->|是| Y4[🟠]
```
**重叠判断公式**
```
预约与时段重叠 = (预约开始时间 < 时段结束时间) AND (预约结束时间 > 时段开始时间)
示例预约15:00-20:00
- 凌晨(0-6): 15 < 6 = false 不重叠 🟢
- 上午(6-12): 15 < 12 = false 不重叠 🟢
- 下午(12-18): 15 < 18 = true AND 20 > 12 = true → 重叠 🟠
- 晚上(18-24): 15 < 24 = true AND 20 > 18 = true → 重叠 🟠
```
---
## 4. 需要改动的文件清单
### 4.1 前端改动
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|------|----------|----------|--------|
| 1 | `pages/appointment/appointment-page.vue` | 将固定时段选择改为时间选择器 | ⭐⭐⭐ |
| 2 | `pages/appointment/book-room-page.vue` | 时段状态显示逻辑优化(可能无需改动,后端已支持) | ⭐ |
| 3 | `components/com/page/reservation-item.vue` | 预约详情显示时间格式调整 | ⭐ |
### 4.2 后端改动
| 序号 | 文件路径 | 改动内容 | 复杂度 |
|------|----------|----------|--------|
| 1 | `SQController.cs` | 接收自定义开始/结束时间参数 | ⭐⭐ |
| 2 | `SQRoomsServices.cs` | 时段状态判断逻辑已支持,可能需微调 | ⭐ |
| 3 | `SQReservationsServices.cs` | 预约冲突检测逻辑验证 | ⭐ |
---
## 5. 详细改动说明
### 5.1 appointment-page.vue 改动
#### 改动前代码
```vue
<template>
<!-- 时段选择(单选按钮) -->
<view class="time-slot-selector">
<view v-for="slot in timeSlotOptions" :key="slot.value"
:class="['slot-item', selectedTimeSlot === slot.value ? 'active' : '']"
@click="selectTimeSlot(slot.value)">
{{ slot.label }}
</view>
</view>
</template>
<script setup>
const selectedTimeSlot = ref(null);
const timeSlotOptions = ref([]);
const calculateTimeFromSlot = () => {
switch (selectedTimeSlot.value) {
case 0: startHour = 0; endHour = 6; break;
case 1: startHour = 6; endHour = 12; break;
case 2: startHour = 12; endHour = 18; break;
case 3: startHour = 18; endHour = 24; break;
}
};
</script>
```
#### 改动后代码
```vue
<template>
<!-- 时间选择器 -->
<view class="time-picker-container">
<view class="time-picker-row">
<text class="label">开始时间</text>
<picker mode="time" :value="startTime" @change="onStartTimeChange">
<view class="picker-value">{{ startTime || '请选择' }}</view>
</picker>
</view>
<view class="time-picker-row">
<text class="label">结束时间</text>
<picker mode="time" :value="endTime" @change="onEndTimeChange">
<view class="picker-value">{{ endTime || '请选择' }}</view>
</picker>
</view>
<view class="time-info">
<text>预计时长: {{ calculateDuration() }}</text>
<text>跨越时段: {{ calculateCrossSlots() }}</text>
</view>
</view>
</template>
<script setup>
const startTime = ref('');
const endTime = ref('');
// 开始时间变更
const onStartTimeChange = (e) => {
startTime.value = e.detail.value;
validateTimeRange();
};
// 结束时间变更
const onEndTimeChange = (e) => {
endTime.value = e.detail.value;
validateTimeRange();
};
// 计算时长
const calculateDuration = () => {
if (!startTime.value || !endTime.value) return '-';
const start = parseTime(startTime.value);
const end = parseTime(endTime.value);
const hours = (end - start) / (1000 * 60 * 60);
return `${hours}小时`;
};
// 计算跨越的时段
const calculateCrossSlots = () => {
if (!startTime.value || !endTime.value) return '-';
const slots = [];
const startHour = parseInt(startTime.value.split(':')[0]);
const endHour = parseInt(endTime.value.split(':')[0]);
if (startHour < 6 || endHour > 0 && endHour <= 6) slots.push('凌晨');
if ((startHour < 12 && endHour > 6) || (startHour >= 6 && startHour < 12)) slots.push('上午');
if ((startHour < 18 && endHour > 12) || (startHour >= 12 && startHour < 18)) slots.push('下午');
if (endHour > 18 || startHour >= 18) slots.push('晚上');
return slots.join('、') || '-';
};
// 验证时间范围
const validateTimeRange = () => {
if (startTime.value && endTime.value) {
const start = parseTime(startTime.value);
const end = parseTime(endTime.value);
if (end <= start) {
uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' });
return false;
}
}
return true;
};
// 构建预约数据(提交时)
const buildReservationData = () => {
const date = new Date(selectedDate.value);
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startDateTime = new Date(date);
startDateTime.setHours(startH, startM, 0, 0);
const endDateTime = new Date(date);
endDateTime.setHours(endH, endM, 0, 0);
return {
start_time: startDateTime.toISOString(),
end_time: endDateTime.toISOString(),
// ... 其他字段
};
};
</script>
```
### 5.2 后端 SQController.cs 改动
#### 改动说明
后端接口已支持接收 `start_time``end_time` 参数,主要验证逻辑需确认:
```csharp
// 添加预约时的冲突检测
[HttpPost("AddSQReservation")]
public async Task<IActionResult> AddSQReservation([FromBody] AddReservationRequest request)
{
// 验证时间有效性
if (request.end_time <= request.start_time)
{
return BadRequest("结束时间必须晚于开始时间");
}
// 检查时段冲突(已有逻辑)
var conflicts = await _reservationService.CheckTimeConflict(
request.room_id,
request.start_time,
request.end_time
);
if (conflicts.Any())
{
return BadRequest("所选时段已被预约");
}
// 创建预约...
}
```
### 5.3 后端时段显示逻辑验证
**文件**: `SQRoomsServices.cs`
当前逻辑已正确支持跨时段检测:
```csharp
// 判断预约是否与时段重叠
bool isReserved = reservations?.Any(r =>
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
```
**验证场景**
- 预约 15:00-20:00
- 下午时段(12:00-18:00): `15:00 < 18:00 && 20:00 > 12:00` = true ✅
- 晚上时段(18:00-24:00): `15:00 < 24:00 && 20:00 > 18:00` = true ✅
**结论**:后端逻辑无需改动,已支持跨时段显示。
---
## 6. 数据流对比
### 6.1 改动前数据流
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant DB as 数据库
U->>F: 选择日期
U->>F: 选择房间
F->>B: 获取房间时段状态
B->>DB: 查询已有预约
DB-->>B: 返回预约列表
B-->>F: 返回4个时段状态
F-->>U: 显示可选时段
U->>F: 选择"下午"时段
F->>F: calculateTimeFromSlot()
Note over F: 固定设置 12:00-18:00
F->>B: 提交预约(12:00-18:00)
B->>DB: 保存预约
DB-->>B: 保存成功
B-->>F: 返回成功
F-->>U: 预约成功
```
### 6.2 改动后数据流
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant DB as 数据库
U->>F: 选择日期
U->>F: 选择房间
F->>B: 获取房间时段状态
B->>DB: 查询已有预约
DB-->>B: 返回预约列表
B-->>F: 返回4个时段状态
F-->>U: 显示时间选择器
U->>F: 选择开始时间 15:00
U->>F: 选择结束时间 20:00
F->>F: validateTimeRange()
F->>F: calculateCrossSlots()
Note over F: 显示"下午、晚上"
F->>B: 提交预约(15:00-20:00)
B->>B: 检查时段冲突
B->>DB: 保存预约
DB-->>B: 保存成功
B-->>F: 返回成功
F-->>U: 预约成功
```
---
## 7. 测试用例
### 7.1 时间选择测试
| 测试场景 | 开始时间 | 结束时间 | 预期结果 |
|----------|----------|----------|----------|
| 正常选择 | 15:00 | 20:00 | 成功,显示跨越"下午、晚上" |
| 同时段选择 | 14:00 | 17:00 | 成功,显示跨越"下午" |
| 跨三时段 | 10:00 | 20:00 | 成功,显示跨越"上午、下午、晚上" |
| 无效时间 | 20:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
| 相同时间 | 15:00 | 15:00 | 失败,提示"结束时间必须晚于开始时间" |
### 7.2 时段冲突测试
| 已有预约 | 新预约 | 预期结果 |
|----------|--------|----------|
| 15:00-20:00 | 10:00-14:00 | 成功(无重叠) |
| 15:00-20:00 | 14:00-16:00 | 失败(重叠) |
| 15:00-20:00 | 19:00-22:00 | 失败(重叠) |
| 15:00-20:00 | 20:00-22:00 | 成功(边界无重叠) |
### 7.3 房间列表显示测试
| 预约时间 | 凌晨 | 上午 | 下午 | 晚上 |
|----------|------|------|------|------|
| 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
| 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
| 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
| 22:00-04:00 | 🟠 | 🟢 | 🟢 | 🟠 |
---
## 8. 工作量评估
| 模块 | 工作内容 | 复杂度 |
|------|----------|--------|
| 前端-预约页面 | 时间选择器组件替换、时间计算逻辑 | ⭐⭐⭐ |
| 前端-房间列表 | 验证显示逻辑(可能无需改动) | ⭐ |
| 前端-预约详情 | 时间格式显示调整 | ⭐ |
| 后端-控制器 | 参数验证、冲突检测 | ⭐⭐ |
| 后端-服务层 | 逻辑验证(可能无需改动) | ⭐ |
| 测试 | 各场景测试 | ⭐⭐ |
**总体评估**中等复杂度改动主要工作集中在前端预约页面的UI和逻辑重构。
---
## 9. 风险与注意事项
1. **向后兼容**:已有的固定时段预约数据仍可正常显示
2. **边界条件**跨天预约如23:00-02:00需特殊处理
3. **时间精度**建议时间选择以30分钟为最小单位
4. **用户体验**:需添加明确的时长提示和时段跨越提示
5. **数据验证**:前后端都需要进行时间有效性验证
---
## 10. 附录:时段判断工具函数
```javascript
/**
* 判断时间范围跨越哪些时段
* @param {number} startHour 开始小时 (0-23)
* @param {number} endHour 结束小时 (0-24)
* @returns {Array} 跨越的时段数组
*/
function getCrossedSlots(startHour, endHour) {
const slots = [];
const slotRanges = [
{ name: '凌晨', start: 0, end: 6 },
{ name: '上午', start: 6, end: 12 },
{ name: '下午', start: 12, end: 18 },
{ name: '晚上', start: 18, end: 24 }
];
for (const slot of slotRanges) {
// 重叠判断:预约开始 < 时段结束 AND 预约结束 > 时段开始
if (startHour < slot.end && endHour > slot.start) {
slots.push(slot.name);
}
}
return slots;
}
// 使用示例
getCrossedSlots(15, 20); // ['下午', '晚上']
getCrossedSlots(10, 14); // ['上午', '下午']
getCrossedSlots(2, 5); // ['凌晨']
```

View File

@ -0,0 +1,665 @@
# 预约时间自由选择 - 开发文档
> 文档版本v1.0
> 更新日期2024年
> 文档类型:开发人员版
---
## 一、改动概述
### 1.1 需求说明
1. 将固定时段选择6小时一段改为自由时间选择
2. 房间列表正确显示跨时段预约的占用状态
### 1.2 影响范围评估
- **前端**主要改动需重构时间选择UI
- **后端**:少量改动,逻辑已基本支持
---
## 二、前端改动详情
### 2.1 appointment-page.vue主要改动
**文件路径**`/uniapp/mahjong_group/pages/appointment/appointment-page.vue`
#### 2.1.1 移除旧的时段选择代码
**需删除的部分**
```vue
<!-- 删除固定时段选择UI -->
<view class="time-slot-selector">
<view v-for="slot in timeSlotOptions" :key="slot.value"
:class="['slot-item', selectedTimeSlot === slot.value ? 'active' : '']"
@click="selectTimeSlot(slot.value)">
{{ slot.label }}
</view>
</view>
```
```javascript
// 删除:固定时段计算函数
const calculateTimeFromSlot = () => {
const date = new Date(selectedDate.value);
let startHour, endHour;
switch (selectedTimeSlot.value) {
case 0: startHour = 0; endHour = 6; break;
case 1: startHour = 6; endHour = 12; break;
case 2: startHour = 12; endHour = 18; break;
case 3: startHour = 18; endHour = 24; break;
}
// ...
};
```
#### 2.1.2 新增时间选择器代码
**Template部分**
```vue
<template>
<!-- 新增:时间选择器 -->
<view class="time-picker-container">
<!-- 开始时间 -->
<view class="time-picker-row">
<text class="label">开始时间</text>
<picker mode="time" :value="startTime" @change="onStartTimeChange">
<view class="picker-value">
{{ startTime || '请选择开始时间' }}
</view>
</picker>
</view>
<!-- 结束时间 -->
<view class="time-picker-row">
<text class="label">结束时间</text>
<picker mode="time" :value="endTime" @change="onEndTimeChange">
<view class="picker-value">
{{ endTime || '请选择结束时间' }}
</view>
</picker>
</view>
<!-- 时间信息展示 -->
<view class="time-info" v-if="startTime && endTime">
<view class="info-item">
<text class="info-label">预计时长</text>
<text class="info-value">{{ calculateDuration() }}</text>
</view>
<view class="info-item">
<text class="info-label">跨越时段</text>
<text class="info-value">{{ calculateCrossSlots() }}</text>
</view>
</view>
<!-- 错误提示 -->
<view class="time-error" v-if="timeError">
<text>{{ timeError }}</text>
</view>
</view>
</template>
```
**Script部分**
```javascript
<script setup>
import { ref, computed } from 'vue';
// 响应式数据
const startTime = ref('');
const endTime = ref('');
const timeError = ref('');
// 开始时间变更
const onStartTimeChange = (e) => {
startTime.value = e.detail.value;
validateTimeRange();
};
// 结束时间变更
const onEndTimeChange = (e) => {
endTime.value = e.detail.value;
validateTimeRange();
};
// 验证时间范围
const validateTimeRange = () => {
timeError.value = '';
if (!startTime.value || !endTime.value) {
return true;
}
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startMinutes = startH * 60 + startM;
const endMinutes = endH * 60 + endM;
if (endMinutes <= startMinutes) {
timeError.value = '结束时间必须晚于开始时间';
return false;
}
// 可选最短时长检查如最少1小时
if (endMinutes - startMinutes < 60) {
timeError.value = '预约时长不能少于1小时';
return false;
}
return true;
};
// 计算时长
const calculateDuration = () => {
if (!startTime.value || !endTime.value) return '-';
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startMinutes = startH * 60 + startM;
const endMinutes = endH * 60 + endM;
const diffMinutes = endMinutes - startMinutes;
if (diffMinutes <= 0) return '-';
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
if (minutes === 0) {
return `${hours}小时`;
}
return `${hours}小时${minutes}分钟`;
};
// 计算跨越的时段
const calculateCrossSlots = () => {
if (!startTime.value || !endTime.value) return '-';
const [startH] = startTime.value.split(':').map(Number);
const [endH] = endTime.value.split(':').map(Number);
const slots = [];
const slotRanges = [
{ name: '凌晨', start: 0, end: 6 },
{ name: '上午', start: 6, end: 12 },
{ name: '下午', start: 12, end: 18 },
{ name: '晚上', start: 18, end: 24 }
];
for (const slot of slotRanges) {
// 重叠判断:预约开始 < 时段结束 AND 预约结束 > 时段开始
if (startH < slot.end && endH > slot.start) {
slots.push(slot.name);
}
}
return slots.join('、') || '-';
};
// 构建预约数据(提交时调用)
const buildReservationData = () => {
if (!validateTimeRange()) {
return null;
}
const date = new Date(selectedDate.value);
const [startH, startM] = startTime.value.split(':').map(Number);
const [endH, endM] = endTime.value.split(':').map(Number);
const startDateTime = new Date(date);
startDateTime.setHours(startH, startM, 0, 0);
const endDateTime = new Date(date);
endDateTime.setHours(endH, endM, 0, 0);
return {
start_time: formatDateTime(startDateTime),
end_time: formatDateTime(endDateTime),
// ... 其他字段保持不变
};
};
// 格式化日期时间
const formatDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = '00';
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 修改提交函数
const submitReservation = async () => {
const data = buildReservationData();
if (!data) {
uni.showToast({ title: timeError.value || '请选择正确的时间', icon: 'none' });
return;
}
// 调用API提交
// ...
};
</script>
```
**Style部分**
```scss
<style lang="scss">
.time-picker-container {
padding: 20rpx;
background-color: #fff;
border-radius: 16rpx;
margin: 20rpx 0;
}
.time-picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #333;
}
.picker-value {
font-size: 28rpx;
color: #666;
padding: 10rpx 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
}
.time-info {
margin-top: 20rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
font-size: 24rpx;
color: #999;
}
.info-value {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
}
.time-error {
margin-top: 16rpx;
padding: 16rpx;
background-color: #fff2f0;
border-radius: 8rpx;
text {
font-size: 24rpx;
color: #ff4d4f;
}
}
</style>
```
### 2.2 book-room-page.vue验证可能无需改动
**文件路径**`/uniapp/mahjong_group/pages/appointment/book-room-page.vue`
当前时段显示已从后端获取数据,后端已支持跨时段检测,**理论上无需前端改动**。
**验证方法**
1. 创建一个15:00-20:00的预约
2. 查看房间列表,确认下午和晚上都显示为已预约状态
如显示不正确,检查后端`SQRoomsServices.cs`中的时段状态返回逻辑。
---
## 三、后端改动详情
### 3.1 SQController.cs验证/微调)
**文件路径**`/server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs`
#### 3.1.1 AddSQReservation 接口验证
当前接口已支持接收`start_time`和`end_time`参数,主要验证:
```csharp
[HttpPost("AddSQReservation")]
public async Task<IActionResult> AddSQReservation([FromBody] AddReservationDto dto)
{
// 验证时间有效性(建议新增)
if (dto.end_time <= dto.start_time)
{
return BadRequest(new { code = 400, msg = "结束时间必须晚于开始时间" });
}
// 验证最短时长可选如最少1小时
var duration = (dto.end_time - dto.start_time).TotalHours;
if (duration < 1)
{
return BadRequest(new { code = 400, msg = "预约时长不能少于1小时" });
}
// 验证最长时长可选如最多12小时
if (duration > 12)
{
return BadRequest(new { code = 400, msg = "预约时长不能超过12小时" });
}
// ... 现有逻辑
}
```
### 3.2 SQRoomsServices.cs验证无需改动
**文件路径**`/server/CoreCms.Net.Services/SQ/SQRoomsServices.cs`
当前时段状态判断逻辑已正确支持跨时段检测:
```csharp
// 现有代码(已支持跨时段)
bool isReserved = reservations?.Any(r =>
r.start_time < slotEnd && r.end_time > slotStart) ?? false;
```
**逻辑验证**
| 预约时间 | 时段 | 判断公式 | 结果 |
|:--------:|:----:|:--------:|:----:|
| 15:00-20:00 | 下午(12-18) | 15 < 18 && 20 > 12 | true ✓ |
| 15:00-20:00 | 晚上(18-24) | 15 < 24 && 20 > 18 | true ✓ |
| 15:00-20:00 | 上午(6-12) | 15 < 12 | false |
**结论**:后端逻辑无需改动。
---
## 四、签到功能代码参考
### 4.1 前端签到组件
**文件路径**`/uniapp/mahjong_group/components/com/page/qiandao-popup.vue`
**签到按钮显示条件**reservation-item.vue
```javascript
// 签到按钮显示条件
const isQianDaoVisible = computed(() => {
const item = props.reservation;
// 1. 预约状态必须为 1已锁定
if (item.status !== 1) return false;
// 2. 只有发起者可签到
if (item.role !== 1) return false;
// 3. 时间范围检查
const now = new Date();
const startTime = new Date(item.start_time);
const endTime = new Date(item.end_time);
const tenMinutes = 10 * 60 * 1000;
const timeToStart = startTime.getTime() - now.getTime();
const timeToEnd = endTime.getTime() - now.getTime();
// 可签到开始前10分钟内 或 已开始但未结束
return (timeToStart <= tenMinutes && timeToStart > 0) ||
(timeToStart <= 0 && timeToEnd > 0);
});
```
### 4.2 后端签到接口
**文件路径**`/server/CoreCms.Net.Web.WebApi/Controllers/SQController.cs`
**方法位置**第1205-1431行 `CheckInReservation`
**请求格式**
```json
POST /api/sq/checkinreservation
{
"reservation_id": 123,
"attendeds": [
{ "user_id": 2, "isAttended": true },
{ "user_id": 3, "isAttended": false }
]
}
```
**核心处理逻辑**
```csharp
// 1. 更新预约状态为"进行中"
reservation.status = 2;
// 2. 处理参与者
foreach (var attendee in dto.attendeds)
{
if (attendee.isAttended)
{
// 已到场:信誉+0.2,标记为已赴约
participant.is_arrive = 1;
user.credit_score = Math.Min(5, user.credit_score + 0.2);
}
else
{
// 未到场:信誉-0.5,鸽子+1踢出
participant.is_arrive = 2;
participant.status = 1; // 已退出
user.credit_score = Math.Max(0, user.credit_score - 0.5);
user.dove_count += 1;
}
}
// 3. 处理押金退款
if (reservation.deposit_fee > 0)
{
// 已到场的参与者:发起退款
// 未到场的参与者:不退款
}
```
---
## 五、数据库相关
### 5.1 相关表结构
**SQReservations预约表**
| 字段 | 类型 | 说明 |
|:----:|:----:|:----:|
| id | int | 主键 |
| status | int | 0=待开始,1=已锁定,2=进行中,3=已结束,4=已取消 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| room_id | int | 房间ID |
| deposit_fee | decimal | 押金金额 |
**SQReservationParticipants参与者表**
| 字段 | 类型 | 说明 |
|:----:|:----:|:----:|
| id | int | 主键 |
| reservation_id | int | 预约ID |
| user_id | int | 用户ID |
| role | int | 0=参与者,1=发起者 |
| status | int | 0=正常,1=已退出 |
| is_arrive | int | 0=默认,1=已赴约,2=未赴约 |
| is_refund | int | 退款状态 |
### 5.2 无需数据库改动
本次改动不涉及数据库结构变更,现有字段已满足需求:
- `start_time` / `end_time` 已支持任意时间
- 时段状态在服务层动态计算
---
## 六、测试用例
### 6.1 时间选择功能测试
| 用例ID | 测试场景 | 输入 | 预期结果 |
|:------:|:--------:|:----:|:--------:|
| T001 | 正常选择 | 开始15:00结束20:00 | 成功,显示"5小时",跨越"下午、晚上" |
| T002 | 同时段 | 开始14:00结束17:00 | 成功,显示"3小时",跨越"下午" |
| T003 | 跨三时段 | 开始10:00结束20:00 | 成功,显示"10小时",跨越"上午、下午、晚上" |
| T004 | 时间倒置 | 开始20:00结束15:00 | 失败,提示"结束时间必须晚于开始时间" |
| T005 | 时间相同 | 开始15:00结束15:00 | 失败,提示"结束时间必须晚于开始时间" |
| T006 | 时长过短 | 开始15:00结束15:30 | 失败,提示"预约时长不能少于1小时" |
### 6.2 时段冲突测试
| 用例ID | 已有预约 | 新预约 | 预期结果 |
|:------:|:--------:|:------:|:--------:|
| T101 | 15:00-20:00 | 10:00-14:00 | 成功(无重叠) |
| T102 | 15:00-20:00 | 14:00-16:00 | 失败(重叠) |
| T103 | 15:00-20:00 | 19:00-22:00 | 失败(重叠) |
| T104 | 15:00-20:00 | 20:00-22:00 | 成功(边界无重叠) |
| T105 | 15:00-20:00 | 12:00-22:00 | 失败(完全包含) |
### 6.3 房间列表显示测试
| 用例ID | 预约时间 | 凌晨 | 上午 | 下午 | 晚上 |
|:------:|:--------:|:----:|:----:|:----:|:----:|
| T201 | 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
| T202 | 05:00-08:00 | 🟠 | 🟠 | 🟢 | 🟢 |
| T203 | 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
| T204 | 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
| T205 | 08:00-22:00 | 🟢 | 🟠 | 🟠 | 🟠 |
### 6.4 签到功能测试
| 用例ID | 测试场景 | 预期结果 |
|:------:|:--------:|:--------:|
| T301 | 开始前11分钟签到 | 签到按钮不显示 |
| T302 | 开始前10分钟签到 | 签到按钮显示,可签到 |
| T303 | 开始后5分钟签到 | 签到按钮显示,可签到 |
| T304 | 结束后签到 | 签到按钮不显示 |
| T305 | 参与者尝试签到 | 签到按钮不显示 |
| T306 | 重复签到 | 提示"已签到,无法重复签到" |
---
## 七、改动文件清单
### 7.1 必须改动
| 序号 | 文件路径 | 改动类型 | 说明 |
|:----:|:--------:|:--------:|:----:|
| 1 | `pages/appointment/appointment-page.vue` | 修改 | 时间选择器重构 |
### 7.2 验证确认
| 序号 | 文件路径 | 改动类型 | 说明 |
|:----:|:--------:|:--------:|:----:|
| 2 | `pages/appointment/book-room-page.vue` | 验证 | 确认跨时段显示正确 |
| 3 | `SQController.cs` | 验证 | 确认时间参数接收正确 |
| 4 | `SQRoomsServices.cs` | 验证 | 确认跨时段检测正确 |
### 7.3 可选优化
| 序号 | 文件路径 | 改动类型 | 说明 |
|:----:|:--------:|:--------:|:----:|
| 5 | `SQController.cs` | 新增 | 添加时长限制验证 |
---
## 八、工具函数
### 8.1 跨时段计算(前端)
```javascript
/**
* 判断时间范围跨越哪些时段
* @param {number} startHour 开始小时 (0-23)
* @param {number} endHour 结束小时 (0-24)
* @returns {Array} 跨越的时段数组
*/
function getCrossedSlots(startHour, endHour) {
const slots = [];
const slotRanges = [
{ name: '凌晨', start: 0, end: 6 },
{ name: '上午', start: 6, end: 12 },
{ name: '下午', start: 12, end: 18 },
{ name: '晚上', start: 18, end: 24 }
];
for (const slot of slotRanges) {
if (startHour < slot.end && endHour > slot.start) {
slots.push(slot.name);
}
}
return slots;
}
// 使用示例
getCrossedSlots(15, 20); // ['下午', '晚上']
getCrossedSlots(10, 14); // ['上午', '下午']
getCrossedSlots(2, 5); // ['凌晨']
```
### 8.2 时长格式化(前端)
```javascript
/**
* 格式化时长显示
* @param {number} minutes 分钟数
* @returns {string} 格式化后的时长
*/
function formatDuration(minutes) {
if (minutes <= 0) return '-';
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours === 0) {
return `${mins}分钟`;
}
if (mins === 0) {
return `${hours}小时`;
}
return `${hours}小时${mins}分钟`;
}
```
---
## 九、注意事项
1. **向后兼容**:已有预约数据不受影响,仍可正常显示
2. **时间精度**UniApp的time picker默认支持分钟级别选择
3. **时区处理**:前后端统一使用本地时间,避免时区转换问题
4. **边界条件**特别注意24:00的处理可视为次日00:00
5. **并发控制**:创建预约时需考虑并发冲突,建议后端加锁

View File

@ -0,0 +1,524 @@
# 预约时间自由选择 - 需求改动文档
> 文档版本v1.0
> 更新日期2024年
> 文档类型:甲方确认版
---
## 一、需求概述
### 1.1 当前问题
- 用户只能选择固定时段(凌晨/上午/下午/晚上每个时段固定6小时
- 无法灵活选择具体的开始和结束时间
- 跨时段预约时,房间列表显示不够直观
### 1.2 改动目标
1. **时段显示优化**预约15:00-20:00时房间列表应显示"下午"和"晚上"都被预定
2. **自由时间选择**:允许用户自由选择具体的开始时间和结束时间
---
## 二、当前系统时段定义
| 时段名称 | 时间范围 | 图标颜色 |
|:--------:|:--------:|:--------:|
| 凌晨 | 00:00 - 06:00 | - |
| 上午 | 06:00 - 12:00 | - |
| 下午 | 12:00 - 18:00 | - |
| 晚上 | 18:00 - 24:00 | - |
**时段状态说明**
- 🟢 可预约 - 该时段空闲,可以预约
- 🟠 已预约 - 该时段已被其他人预约
- ⚫ 不可用 - 该时段不开放
- 🔵 使用中 - 该时段正在使用
---
## 三、完整业务闭环
### 3.1 发起者完整闭环(创建预约→结束)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 发起者完整业务闭环 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────┐
│ 发起者 │
└────┬─────┘
┌─────────────────┐
│ 1. 选择日期 │
└────────┬────────┘
┌─────────────────┐
│ 2. 选择房间 │ ←─── 查看房间时段状态
└────────┬────────┘ 🟢可预约 🟠已预约
┌─────────────────┐
│ 3. 选择时段 │ ←─── 【改动点】改为自由选择时间
└────────┬────────┘
┌─────────────────┐
│ 4. 填写预约信息 │ ←─── 标题、人数、要求等
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ 5. 支付押金 │────▶│ 创建成功 │
│ (如需要) │ │ 状态:待开始 │
└─────────────────┘ └────────┬────────┘
┌───────────────────────┤
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 等待参与者加入 │ │ 分享给好友 │
└────────┬────────┘ └─────────────────┘
│ 【人满/时间到】
┌─────────────────┐
│ 状态:已锁定 │ ←─── 不可再加入/退出
└────────┬────────┘
│ 【开始前10分钟~开始后】
┌─────────────────┐
│ 6. 签到确认 ⭐ │ ←─── 确认参与者到场情况
└────────┬────────┘
├─────────────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 参与者已到场 │ │ 参与者未到场 │
│ 信誉分 +0.2 │ │ 信誉分 -0.5 │
│ 退还押金 │ │ 鸽子数 +1 │
└────────┬────────┘ │ 押金不退 │
│ └─────────────────┘
┌─────────────────┐
│ 状态:进行中 │ ←─── 预约正式开始
└────────┬────────┘
│ 【时间结束】
┌─────────────────┐
│ 7. 互相评价 │ ←─── 牌品、牌技评分
└────────┬────────┘
┌─────────────────┐
│ 状态:已结束 │ ←─── 预约完成
└─────────────────┘
```
### 3.2 参与者完整闭环(加入预约→结束)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 参与者完整业务闭环 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────┐
│ 参与者 │
└────┬─────┘
├───────────────────┬───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 首页浏览 │ │ 好友分享 │ │ 扫码进入 │
│ 麻将局列表 │ │ 点击链接 │ │ │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────┴───────────────────┘
┌─────────────────┐
│ 1. 查看预约详情 │
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ 2. 点击加入 │────▶│ 检查是否在 │
└─────────────────┘ │ 黑名单中 │
└────────┬────────┘
┌───────────────────────┴───────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 在黑名单中 │ │ 不在黑名单 │
│ 无法加入 ❌ │ └────────┬────────┘
└─────────────────┘ │
┌─────────────────┐
│ 3. 支付押金 │
│ (如需要) │
└────────┬────────┘
┌─────────────────┐
│ 加入成功 ✓ │
│ 等待预约开始 │
└────────┬────────┘
┌──────────────────────────────────────────────────┤
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 主动退出预约 │ │ 等待锁定 │
│ (锁定前可退) │ └────────┬────────┘
└────────┬────────┘ │
│ │
▼ │ 【人满/时间到】
┌─────────────────┐ ▼
│ 退还押金 │ ┌─────────────────┐
│ 退出成功 │ │ 状态:已锁定 │
└─────────────────┘ └────────┬────────┘
│ 【发起者签到】
┌─────────────────┐
│ 4. 被签到确认 │
└────────┬────────┘
┌──────────────────────────────────────────────────┤
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 已到场 ✓ │ │ 未到场 ✗ │
│ 信誉分 +0.2 │ │ 信誉分 -0.5 │
│ 押金退还 │ │ 鸽子数 +1 │
└────────┬────────┘ │ 押金不退 │
│ │ 强制退出 │
│ └─────────────────┘
┌─────────────────┐
│ 状态:进行中 │
└────────┬────────┘
│ 【时间结束】
┌─────────────────┐
│ 5. 互相评价 │
└────────┬────────┘
┌─────────────────┐
│ 状态:已结束 │
└─────────────────┘
```
---
## 四、签到功能详解
### 4.1 签到流程图
```mermaid
flowchart TD
A[预约状态:已锁定] --> B{时间判断}
B -->|开始前10分钟内| C[显示签到按钮]
B -->|预约已开始且未结束| C
B -->|时间未到| D[签到按钮隐藏]
C --> E[发起者点击签到]
E --> F[弹出签到确认窗口]
F --> G[显示参与者列表]
G --> H{逐个确认到场}
H -->|已到场| I[标记为到场 ✓]
H -->|未到场| J[标记为缺席 ✗]
I --> K[提交签到]
J --> K
K --> L{系统处理}
L --> M[到场者:信誉+0.2,退押金]
L --> N[缺席者:信誉-0.5,扣押金,踢出]
M --> O[预约状态→进行中]
N --> O
```
### 4.2 签到界面原型
```
┌─────────────────────────────────────────────────────────────────┐
│ 到场确认 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 请确认以下参与者是否已到场: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 👤 张三 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ ✓ 已到场 │ │ ✗ 未到场 │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 👤 李四 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ ✓ 已到场 │ │ ✗ 未到场 │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 👤 王五 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ ✓ 已到场 │ │ ✗ 未到场 │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 提示:标记为"未到场"的参与者将被扣除信誉分 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 确认签到 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 4.3 签到规则说明
| 项目 | 说明 |
|:----:|:----:|
| **签到人** | 仅发起者可以签到 |
| **签到时间** | 预约开始前10分钟 ~ 预约结束时间 |
| **签到次数** | 每个预约只能签到一次 |
| **到场奖励** | 信誉分 +0.2上限5分 |
| **缺席惩罚** | 信誉分 -0.5,鸽子数 +1 |
| **押金处理** | 到场者退还,缺席者不退 |
---
## 五、时间选择改动对比
### 5.1 改动前(固定时段选择)
```
┌─────────────────────────────────────────────────────────────────┐
│ 选择预约时段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ○ 凌晨 (00:00 - 06:00) │
│ │
│ ○ 上午 (06:00 - 12:00) │
│ │
│ ● 下午 (12:00 - 18:00) ← 只能选择一个完整时段 │
│ │
│ ○ 晚上 (18:00 - 24:00) │
│ │
│ ⚠️ 限制只能选择固定6小时时段无法自定义时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 5.2 改动后(自由时间选择)
```
┌─────────────────────────────────────────────────────────────────┐
│ 选择预约时间 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 开始时间 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 15 : 00 ▼ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 结束时间 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 20 : 00 ▼ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 📊 预计时长5 小时 │ │
│ │ 📅 跨越时段:下午、晚上 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 💡 提示:结束时间必须晚于开始时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 5.3 房间列表显示对比
**场景**:某房间已被预约 15:00 - 20:00
```
【改动前】房间时段显示:
┌─────────────────────────────────────────────────────────────────┐
│ 304包厢 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 凌晨 │ │ 上午 │ │ 下午 │ │ 晚上 │ │
│ │ 🟢 │ │ 🟢 │ │ 🟠 │ │ 🟢 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ ❌ 问题晚上时段显示为可预约但实际18:00-20:00已被占用 │
└─────────────────────────────────────────────────────────────────┘
【改动后】房间时段显示:
┌─────────────────────────────────────────────────────────────────┐
│ 304包厢 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 凌晨 │ │ 上午 │ │ 下午 │ │ 晚上 │ │
│ │ 🟢 │ │ 🟢 │ │ 🟠 │ │ 🟠 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ ✓ 正确:下午和晚上都显示为已预约状态 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 六、预约状态流转图
```mermaid
stateDiagram-v2
[*] --> 待开始: 创建预约
待开始 --> 已锁定: 人满或时间临近
待开始 --> 已取消: 发起者取消
已锁定 --> 进行中: 发起者签到
已锁定 --> 已取消: 发起者取消(需扣分)
进行中 --> 已结束: 时间结束
已取消 --> [*]
已结束 --> [*]
note right of 待开始
可加入/退出
可分享
end note
note right of 已锁定
不可加入/退出
等待签到
end note
note right of 进行中
正在进行
可评价
end note
```
### 状态说明表
| 状态 | 状态码 | 可执行操作 | 说明 |
|:----:|:------:|:----------:|:----:|
| 待开始 | 0 | 加入、退出、取消、分享 | 预约创建后的初始状态 |
| 已锁定 | 1 | 签到、取消 | 人满或临近开始时间 |
| 进行中 | 2 | 评价 | 签到后预约正式开始 |
| 已结束 | 3 | 查看记录 | 预约时间结束 |
| 已取消 | 4 | 无 | 预约被取消 |
---
## 七、信誉分规则
### 7.1 信誉分变化场景
| 场景 | 分数变化 | 说明 |
|:----:|:--------:|:----:|
| 按时赴约 | +0.2 | 签到时标记为"已到场" |
| 放鸽子 | -0.5 | 签到时标记为"未到场" |
| 取消预约(锁定前) | 无变化 | 正常取消 |
| 取消预约(锁定后) | -0.3 | 临时取消扣分 |
### 7.2 信誉分等级
| 等级 | 分数范围 | 说明 |
|:----:|:--------:|:----:|
| 优秀 | 4.5 - 5.0 | 信誉极好 |
| 良好 | 4.0 - 4.4 | 信誉较好 |
| 一般 | 3.0 - 3.9 | 信誉一般 |
| 较差 | 2.0 - 2.9 | 需要注意 |
| 很差 | < 2.0 | 可能被限制 |
---
## 八、改动工作内容总结
### 8.1 前端改动
| 序号 | 页面/组件 | 改动内容 |
|:----:|:--------:|:--------:|
| 1 | 创建预约页面 | 将时段单选改为时间选择器 |
| 2 | 创建预约页面 | 增加时长计算和跨时段提示 |
| 3 | 房间列表页面 | 验证跨时段显示(可能无需改动) |
### 8.2 后端改动
| 序号 | 模块 | 改动内容 |
|:----:|:----:|:--------:|
| 1 | 预约接口 | 支持接收自定义开始/结束时间 |
| 2 | 冲突检测 | 验证跨时段冲突检测逻辑 |
### 8.3 测试验证
| 序号 | 测试场景 | 预期结果 |
|:----:|:--------:|:--------:|
| 1 | 选择15:00-20:00 | 成功,显示跨越"下午、晚上" |
| 2 | 选择20:00-15:00 | 失败,提示时间无效 |
| 3 | 房间已有15:00-20:00预约 | 下午🟠、晚上🟠 |
| 4 | 预约10:00-14:00 | 显示跨越"上午、下午" |
---
## 九、待确认问题
请甲方确认以下问题:
### 问题1时间选择精度
- [ ] 选项A以30分钟为最小单位如15:00、15:30、16:00
- [ ] 选项B以1小时为最小单位如15:00、16:00、17:00
- [ ] 选项C以15分钟为最小单位
### 问题2跨天预约
- [ ] 是否支持跨天预约如23:00 - 次日02:00
- [ ] 如支持,如何处理跨天的时段显示?
### 问题3最短/最长预约时长
- [ ] 最短预约时长限制____小时
- [ ] 最长预约时长限制____小时
### 问题4时间冲突提示
- [ ] 选项A选择时间时实时检测冲突
- [ ] 选项B提交时统一检测冲突
---
## 十、附录
### A. 名词解释
| 名词 | 解释 |
|:----:|:----:|
| 发起者 | 创建预约的用户 |
| 参与者 | 加入预约的其他用户 |
| 签到 | 发起者确认参与者到场情况的操作 |
| 锁定 | 预约人满或临近开始,不可再加入/退出 |
| 鸽子数 | 用户放鸽子(未到场)的累计次数 |
### B. 时段重叠判断示例
| 预约时间 | 凌晨(0-6) | 上午(6-12) | 下午(12-18) | 晚上(18-24) |
|:--------:|:---------:|:----------:|:-----------:|:-----------:|
| 02:00-05:00 | 🟠 | 🟢 | 🟢 | 🟢 |
| 05:00-08:00 | 🟠 | 🟠 | 🟢 | 🟢 |
| 10:00-14:00 | 🟢 | 🟠 | 🟠 | 🟢 |
| 15:00-20:00 | 🟢 | 🟢 | 🟠 | 🟠 |
| 22:00-02:00 | 🟠 | 🟢 | 🟢 | 🟠 |