Merge local friend_back v2 including worktree changes

This commit is contained in:
dzc committed 2026-10-29 16:38:58 +08:00
commit 9811d336e0
156 files changed
+6580 -186

No files matched your search

+283
View File
@@ -0,0 +1,283 @@
# 清库操作文档:清空全部用户数据(含 IM)
> 适用环境:荷塘月色业务后端 `friend_back` + IM 系统 `chat-im`
> 目标:清除**所有会员用户**相关数据(资料、会话、钱包/分销用户侧数据、IM 好友与聊天等)
> **不是**「注销单个用户」;也不是清空系统配置/管理员账号。
---
## 1. 先搞清楚有两套系统
| 系统 | 代码目录 | MySQL 库 | Redis | 文件 |
|------|----------|----------|-------|------|
| 业务 API | `friend_back` | **`chat_v1`** | **db 7** | `infra_file` + 对应对象存储(七牛/MinIO 等) |
| IM | `chat-im`(`im-platform` / `im-server`) | **`im_platform`** | **db 10** | MinIO bucket **`im-platform`** |
- 会员 ID 对齐:`member_user.id` == `im_user.id`
- `friend_back` 只通过 HTTP 调 IM(`chat.base-url`),**没有「删除用户」的第三方 API**,IM 必须在 `im_platform` 库直接清
- 特殊 IM 账号(**不要删**):
- `-100` 系统消息通知号
- `-200` 客服号
生产配置参考:
- 业务:`friend_back/yudao-server/src/main/resources/application-prod.yaml`(`chat_v1`、Redis db7、`chat.base-url`)
- IM:`chat-im/im-platform/src/main/resources/application-prod.yml`(`im_platform`、Redis db10)
---
## 2. 操作前检查清单
1. **确认环境**(测试 / 预发 / 生产),禁止在未备份的生产库直接执行。
2. **全量备份**
```bash
mysqldump -h... -u... -p chat_v1 > chat_v1_backup_$(date +%Y%m%d).sql
mysqldump -h... -u... -p im_platform > im_platform_backup_$(date +%Y%m%d).sql
# Redis:按运维规范导出 RDB/AOF(业务 db7、IM db10)
# 对象存储:按需快照或确认可丢弃用户上传文件
```
3. **停写流量**(强烈建议)
- 停或摘流:`yudao-server`、`im-platform`、`im-server`
- 避免删一半又有新注册/新消息写入
4. **合规确认**
- 支付订单、退款、提现流水是否允许物理删除(默认建议**保留或脱敏归档**,见第 5 节)
5. **现有后台没有「一键清会员」接口**,只能运维 SQL + Redis + 存储清理。
---
## 3. 建议操作顺序(总览)
```text
① 停服务
② 备份 MySQL / Redis / 文件
③ 清业务库 chat_v1(用户相关表)
④ 清 IM 库 im_platform(保留 -100/-200)
⑤ 清 Redis db7、db10
⑥ 清用户上传文件(可选)
⑦ 启服务并验收
```
---
## 4. 业务库 `chat_v1`:清用户数据
> 执行前先 `USE chat_v1;`
> 下面按「先子表、后主表」;若表不存在可跳过。
> 全量重置测试环境可用 `TRUNCATE`;生产更建议 `DELETE` + 条件(如按 `tenant_id`)。
### 4.1 会员扩展与行为(必清)
```sql
-- 相册 / 位置 / 动态相关
TRUNCATE TABLE member_photo;
TRUNCATE TABLE member_location;
TRUNCATE TABLE member_recent;
TRUNCATE TABLE member_operation;
TRUNCATE TABLE member_view_record;
TRUNCATE TABLE member_recent_tag;
TRUNCATE TABLE member_recent_cfg;
-- 认证 / 反馈 / 开通记录 / 积分经验签到
TRUNCATE TABLE member_com_auth;
TRUNCATE TABLE member_feedback;
TRUNCATE TABLE member_open_type_user;
TRUNCATE TABLE member_point_record;
TRUNCATE TABLE member_level_record;
TRUNCATE TABLE member_experience_record;
TRUNCATE TABLE member_sign_in_record;
TRUNCATE TABLE member_address;
```
### 4.2 登录态 / 社交绑定(会员侧)
```sql
-- user_type = 1 表示会员(Member)
DELETE FROM system_oauth2_access_token WHERE user_type = 1;
DELETE FROM system_oauth2_refresh_token WHERE user_type = 1;
DELETE FROM system_oauth2_approve WHERE user_type = 1;
DELETE FROM system_oauth2_code WHERE user_type = 1;
DELETE FROM system_social_user_bind WHERE user_type = 1;
-- 可选:清理已无绑定的 social_user 孤儿数据(按你们库实际字段调整)
DELETE FROM system_login_log WHERE user_type = 1;
DELETE FROM system_notify_message WHERE user_type = 1;
-- 短信验证码可按需清空
TRUNCATE TABLE system_sms_code;
```
### 4.3 分销用户数据(必清用户侧)
```sql
TRUNCATE TABLE trade_brokerage_record;
TRUNCATE TABLE trade_brokerage_withdraw;
TRUNCATE TABLE trade_brokerage_code;
TRUNCATE TABLE trade_brokerage_user;
```
### 4.4 钱包用户数据(必清用户侧)
```sql
TRUNCATE TABLE pay_wallet_transaction;
TRUNCATE TABLE pay_wallet_recharge;
TRUNCATE TABLE pay_wallet_user_transition; -- 若存在
TRUNCATE TABLE pay_wallet_tx_auth; -- 若存在
DELETE FROM pay_wallet WHERE user_type = 1;
```
### 4.5 商城用户行为(若启用 mall)
```sql
TRUNCATE TABLE trade_cart;
TRUNCATE TABLE trade_order_log;
TRUNCATE TABLE trade_order_item;
TRUNCATE TABLE trade_after_sale_log;
TRUNCATE TABLE trade_after_sale;
TRUNCATE TABLE trade_order;
TRUNCATE TABLE product_browse_history;
TRUNCATE TABLE product_favorite;
TRUNCATE TABLE product_comment;
-- 优惠券领取记录等带 user_id 的 promotion_* 表按实际启用情况清理
```
### 4.6 最后删会员主表
```sql
TRUNCATE TABLE member_user;
-- 或:DELETE FROM member_user;
```
### 4.7 业务库「不要清」的配置表(示例)
保留系统能继续跑的配置:
- `member_config`、`member_level`、`member_tag`、`member_group`
- `member_sign_in_config`、`member_open_type`(会员套餐)
- `pay_app`、`pay_channel`、`pay_wallet_recharge_package`
- `trade_config`
- `system_oauth2_client`、`system_social_client`、短信模板/渠道
- `system_users` 等**后台管理员**账号(除非连运营账号一起重置)
---
## 5. 支付流水是否删除(单独决策)
| 策略 | 处理 |
|------|------|
| **推荐(测试可删、生产慎删)** | 保留 `pay_order` / `pay_order_extension` / `pay_refund` / `pay_transfer` / `pay_notify_*`,仅清用户侧钱包与会员 |
| **测试环境彻底重置** | 可一并 `TRUNCATE` 上述支付流水表,并确认与渠道对账无冲突 |
若生产必须销毁支付痕迹,需法务/财务确认后另出工单,不建议与「清用户」混为一谈。
---
## 6. IM 库 `im_platform`:清 IM 数据
> 执行前先 `USE im_platform;`
> 建表参考:`chat-im/db/im-platform.sql`
```sql
-- 消息与关系
TRUNCATE TABLE im_private_message;
TRUNCATE TABLE im_group_message;
TRUNCATE TABLE im_friend;
TRUNCATE TABLE im_group_member;
TRUNCATE TABLE im_group;
TRUNCATE TABLE im_file_info;
-- 用户:保留系统号
DELETE FROM im_user WHERE id NOT IN (-100, -200);
-- 敏感词库一般保留
-- TRUNCATE TABLE im_sensitive_word; -- 仅当要重置词库时
```
**说明:**
- 不要依赖 `friend_back` 的 ChatApi 删用户(接口不支持)
- 清完后会员重新登录会走同步逻辑(`syncChatUser`)在 IM 侧重建用户
---
## 7. Redis
**务必指定 DB,禁止误 FLUSH 其它库。**
```bash
# 业务 Redis(application-prod: database 7)
redis-cli -n 7 FLUSHDB
# IM Redis(im-platform/im-server: database 10)
redis-cli -n 10 FLUSHDB
```
若不能整库清空,至少清理:
- 业务:`oauth2_access_token:*`、会员相关 lock/cache、`social_auth_state:*` 等
- IM:`im:user:*`、`im:cache:*`、`im:message:*`、`im:readed:*` 等
---
## 8. 文件存储(可选)
| 来源 | 处理建议 |
|------|----------|
| 业务头像/相册/认证图 | 根据 `infra_file` + `member_user.avatar` / `member_photo` 对应对象存储清理;测试环境可按配置整桶策略清理 |
| IM 图片视频 | MinIO bucket `im-platform`;清 `im_file_info` 后按 path 删对象,或测试环境整桶清理 |
生产环境删除对象存储前务必确认无合规留存要求。
---
## 9. 启动与验收
1. 启动 `im-platform` → `im-server` → `yudao-server`
2. 检查:
```sql
SELECT COUNT(*) FROM chat_v1.member_user; -- 期望 0
SELECT COUNT(*) FROM im_platform.im_user
WHERE id NOT IN (-100, -200); -- 期望 0
SELECT id, user_name FROM im_platform.im_user; -- 应还能看到 -100/-200
```
3. App 侧:
- 旧 token 应失效,需重新注册/登录
- 登录后 IM 可正常建链、发消息
- 后台管理账号仍可登录(若未清 `system_users`)
4. 配置仍在:会员套餐、支付渠道、分销开关、敏感词等
---
## 10. 回滚
仅能依赖操作前备份:
```bash
mysql -h... -u... -p chat_v1 < chat_v1_backup_YYYYMMDD.sql
mysql -h... -u... -p im_platform < im_platform_backup_YYYYMMDD.sql
# 恢复 Redis / 文件快照后重启服务
```
---
## 11. 常见问题
**Q:只清 `member_user` 行不行?**
不行。登录 token、IM 消息、钱包、分销、相册等都会残留,且可能脏数据关联异常。
**Q:清完业务库,IM 会不会自动空?**
不会。必须单独清 `im_platform`(和 Redis db10)。
**Q:有没有管理后台按钮?**
没有会员「注销/全量删除」接口;管理员删除只作用于后台 `system_users`。
**Q:测试环境最快怎么干?**
停服务 → 备份 → 按第 4~7 节 TRUNCATE/DELETE + `FLUSHDB` → 启服务。
---
## 12. 变更记录
| 日期 | 说明 |
|------|------|
| 2026-08-12 | 初版:基于当前 `friend_back` + `chat-im` 配置与表结构整理 |
-9
View File
@@ -16,17 +16,8 @@
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<module>yudao-module-member</module>
<!-- <module>yudao-module-bpm</module>-->
<!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>-->
<module>yudao-module-pay</module>
<module>yudao-module-mall</module>
<!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>-->
<!-- <module>yudao-module-iot</module>-->
<!-- AI 大模型的开启,请参考 https://doc.iocoder.cn/ai/build/ 文档,对 JDK 版本要要求! -->
<!-- <module>yudao-module-ai</module>-->
<!-- <module>yudao-module-iot</module>-->
</modules>
<name>${project.artifactId}</name>
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eE
Ud2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1y
wsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/
Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6o
mstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZx
AMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3J
p09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6M
kqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebt
pV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtB
UKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdP
uHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgL
DtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX
6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJS
OanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8o
ZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+W
ijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwv
O+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD
1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPY
w5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0V
FF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2
SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMx
yoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRsw
HDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585
E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD
1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4
pdry0NF+Wfjny8ATyFDKY08w
-----END PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJ
a1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7Sd
eQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Z
r6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMp
BWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVW
ceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26k
DwIDAQAB
-----END PUBLIC KEY-----
+1
View File
@@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJa1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7SdeQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Zr6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMpBWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVWceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26kDwIDAQAB
@@ -0,0 +1,64 @@
-- 航空账号(管理员开通:手机号+密码+有效期+设备唯一绑定)
CREATE TABLE IF NOT EXISTS `member_airline_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`mobile` varchar(11) NOT NULL COMMENT '手机号(同一手机号可在不同渠道各有一个账号)',
`channel` varchar(8) NOT NULL DEFAULT 'CZ' COMMENT '品牌渠道(CZ南航/MU东航/HU海航)',
`password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码(BCrypt)',
`start_time` datetime NOT NULL COMMENT '生效开始时间',
`end_time` datetime NOT NULL COMMENT '生效结束时间',
`device_id` varchar(128) NULL DEFAULT NULL COMMENT '手机唯一识别号(首次登录绑定;重装/重启应由客户端保持不变)',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0开启 1关闭)',
`remark` varchar(500) NULL DEFAULT NULL COMMENT '备注',
`user_id` bigint NULL DEFAULT NULL COMMENT '关联会员用户编号 member_user.id(已废弃,登录不再创建会员用户)',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_mobile_channel` (`mobile`, `channel`, `deleted`, `tenant_id`) USING BTREE,
UNIQUE KEY `uk_channel_device` (`channel`, `device_id`, `deleted`, `tenant_id`) USING BTREE,
KEY `idx_end_time` (`end_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='航空账号';
-- 菜单:航空账号管理(挂在会员中心 2262 下)
-- 若 id 冲突,请在管理后台「菜单管理」手动新建,或调整 id
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (5800, '航空账号', '', 2, 15, 2262, 'airline-account', 'ep:iphone', 'member/airlineAccount/index', 'MemberAirlineAccount', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `component` = VALUES(`component`);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(5801, '航空账号查询', 'member:airline-account:query', 3, 1, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5802, '航空账号创建', 'member:airline-account:create', 3, 2, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5803, '航空账号更新', 'member:airline-account:update', 3, 3, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5804, '航空账号删除', 'member:airline-account:delete', 3, 4, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5805, '航空账号解绑设备', 'member:airline-account:unbind', 3, 5, 5800, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`);
-- 测试账号(App 登录)
-- 手机号:13800138000 密码:123456 有效期约一年 未绑定设备
-- 密码哈希为 BCrypt(123456)
INSERT INTO `member_airline_account`
(`mobile`, `channel`, `password`, `start_time`, `end_time`, `device_id`, `status`, `remark`, `user_id`, `tenant_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT '13800138000',
'CZ',
'$2a$10$PplOV8yXcQo9xc32V8vwTuIMbMKgNtjXf3lu6l18LUTOy9YGDdAY6',
NOW() - INTERVAL 1 DAY,
NOW() + INTERVAL 365 DAY,
NULL,
0,
'本地联调测试账号',
NULL,
1,
'1',
NOW(),
'1',
NOW(),
b'0'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `member_airline_account`
WHERE `mobile` = '13800138000' AND `channel` = 'CZ' AND `deleted` = b'0' AND `tenant_id` = 1
);
@@ -0,0 +1,20 @@
-- 航空账号增加品牌渠道(channel):CZ 南航 / MU 东航 / HU 海航
-- 目的:三个 App 使用同一手机号分别登录,互不干扰。
-- 唯一键调整:
-- uk_mobile (mobile, deleted, tenant_id) -> 废弃
-- uk_device_id (device_id, deleted, tenant_id) -> 废弃
-- uk_mobile_channel (mobile, channel, deleted, tenant_id) -> 同一手机号每渠道一个账号
-- uk_channel_device (channel, device_id, deleted, tenant_id) -> 同一设备每渠道绑定一个账号
ALTER TABLE `member_airline_account`
ADD COLUMN `channel` varchar(8) NOT NULL DEFAULT 'CZ' COMMENT '品牌渠道(CZ南航/MU东航/HU海航)' AFTER `mobile`;
-- 存量数据归属渠道默认回填为 CZ(南航),如实际属于东航/海航请手工执行:
-- UPDATE `member_airline_account` SET `channel` = 'MU' WHERE ...;
-- UPDATE `member_airline_account` SET `channel` = 'HU' WHERE ...;
ALTER TABLE `member_airline_account`
DROP INDEX `uk_mobile`,
DROP INDEX `uk_device_id`,
ADD UNIQUE KEY `uk_mobile_channel` (`mobile`, `channel`, `deleted`, `tenant_id`),
ADD UNIQUE KEY `uk_channel_device` (`channel`, `device_id`, `deleted`, `tenant_id`);
+64
View File
@@ -0,0 +1,64 @@
-- ----------------------------------------------------------------------------
-- 航空 App 数据云同步:新增航线 / 我的(积点钱包)
-- 数据归属:member_airline_account.id(登录账号),一账号一套数据
-- ----------------------------------------------------------------------------
-- 用户自建的飞行任务(航线)
CREATE TABLE IF NOT EXISTS `member_airline_flight` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`account_id` bigint NOT NULL COMMENT '航空账号编号 member_airline_account.id',
`flight_no` varchar(32) NULL DEFAULT '' COMMENT '航班号',
`title` varchar(64) NULL DEFAULT '' COMMENT '任务/航班名称',
`begin_city` varchar(64) NULL DEFAULT '' COMMENT '出发城市',
`begin_airport` varchar(128) NULL DEFAULT '' COMMENT '出发机场',
`end_city` varchar(64) NULL DEFAULT '' COMMENT '到达城市',
`end_airport` varchar(128) NULL DEFAULT '' COMMENT '到达机场',
`classes` varchar(128) NULL DEFAULT '' COMMENT '舱位/班次',
`status` varchar(32) NULL DEFAULT '' COMMENT '状态',
`fly_begin_date0` varchar(32) NULL DEFAULT '' COMMENT '计划起飞时间',
`fly_end_date0` varchar(32) NULL DEFAULT '' COMMENT '计划到达时间',
`fly_begin_date1` varchar(32) NULL DEFAULT '' COMMENT '实际起飞时间',
`fly_end_date1` varchar(32) NULL DEFAULT '' COMMENT '实际到达时间',
`sign_in_time` varchar(32) NULL DEFAULT '' COMMENT '签到时间',
`aircraft_no` varchar(32) NULL DEFAULT '' COMMENT '机号/编号',
`airline` varchar(64) NULL DEFAULT '' COMMENT '航司',
`aircraft_type` varchar(64) NULL DEFAULT '' COMMENT '机型',
`gate` varchar(32) NULL DEFAULT '' COMMENT '登机口',
`begin_stand` varchar(32) NULL DEFAULT '' COMMENT '出发机场停机位',
`end_stand` varchar(32) NULL DEFAULT '' COMMENT '到达机场停机位',
`cargo_close` varchar(32) NULL DEFAULT '' COMMENT '货关时间',
`pax_close` varchar(32) NULL DEFAULT '' COMMENT '客关时间',
`leave_port` varchar(32) NULL DEFAULT '' COMMENT '离港时间',
`take_off` varchar(32) NULL DEFAULT '' COMMENT '起飞时间',
`land` varchar(32) NULL DEFAULT '' COMMENT '落地时间',
`arrive_port` varchar(32) NULL DEFAULT '' COMMENT '到港时间',
`prev_flight` varchar(255) NULL DEFAULT '' COMMENT '前序航班',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_account_id` (`account_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='航空App用户自建航线';
-- 我的页面:积点/钱包(每账号一行)
CREATE TABLE IF NOT EXISTS `member_airline_point` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`account_id` bigint NOT NULL COMMENT '航空账号编号 member_airline_account.id',
`current_month` varchar(16) NULL DEFAULT '00' COMMENT '当月飞行',
`current_year` varchar(16) NULL DEFAULT '00' COMMENT '年度飞行',
`jixiao` varchar(16) NULL DEFAULT '00' COMMENT '绩效积点',
`yanwu` varchar(16) NULL DEFAULT '00' COMMENT '延误积点',
`current_point` varchar(16) NULL DEFAULT '00' COMMENT '当前积点',
`wallet` varchar(16) NULL DEFAULT '00' COMMENT '钱包余额',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_account_id` (`account_id`, `deleted`, `tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='航空App我的积点钱包';
@@ -0,0 +1,21 @@
-- ----------------------------------------------------------------------------
-- member_airline_flight 补齐航线详情字段(登机口/货关/客关/停机位/离港/起飞/落地/到港等)
-- 背景:早期云同步表只建了 15 个基础字段,客户端上报的详情字段被服务端丢弃,
-- 拉取后详情页登机口、货关、客关等为空。本脚本为存量库补列。
-- 已执行过本脚本的库无需重复执行(列已存在会报 Duplicate column)。
-- ----------------------------------------------------------------------------
ALTER TABLE `member_airline_flight`
ADD COLUMN `aircraft_no` varchar(32) NULL DEFAULT '' COMMENT '机号/编号' AFTER `sign_in_time`,
ADD COLUMN `airline` varchar(64) NULL DEFAULT '' COMMENT '航司' AFTER `aircraft_no`,
ADD COLUMN `aircraft_type` varchar(64) NULL DEFAULT '' COMMENT '机型' AFTER `airline`,
ADD COLUMN `gate` varchar(32) NULL DEFAULT '' COMMENT '登机口' AFTER `aircraft_type`,
ADD COLUMN `begin_stand` varchar(32) NULL DEFAULT '' COMMENT '出发机场停机位' AFTER `gate`,
ADD COLUMN `end_stand` varchar(32) NULL DEFAULT '' COMMENT '到达机场停机位' AFTER `begin_stand`,
ADD COLUMN `cargo_close` varchar(32) NULL DEFAULT '' COMMENT '货关时间' AFTER `end_stand`,
ADD COLUMN `pax_close` varchar(32) NULL DEFAULT '' COMMENT '客关时间' AFTER `cargo_close`,
ADD COLUMN `leave_port` varchar(32) NULL DEFAULT '' COMMENT '离港时间' AFTER `pax_close`,
ADD COLUMN `take_off` varchar(32) NULL DEFAULT '' COMMENT '起飞时间' AFTER `leave_port`,
ADD COLUMN `land` varchar(32) NULL DEFAULT '' COMMENT '落地时间' AFTER `take_off`,
ADD COLUMN `arrive_port` varchar(32) NULL DEFAULT '' COMMENT '到港时间' AFTER `land`,
ADD COLUMN `prev_flight` varchar(255) NULL DEFAULT '' COMMENT '前序航班' AFTER `arrive_port`;
+18
View File
@@ -0,0 +1,18 @@
-- 会员互动消息(被查看 / 微信被解锁)
CREATE TABLE IF NOT EXISTS `member_interaction` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`receiver_id` bigint NOT NULL COMMENT '接收人(被查看 / 被解锁用户)',
`actor_id` bigint NOT NULL COMMENT '操作人',
`biz_type` varchar(32) NOT NULL COMMENT 'VIEWED / WECHAT_UNLOCKED',
`ref_id` bigint NULL DEFAULT NULL COMMENT '关联业务 id(访客记录 / 操作记录)',
`read_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已读',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_receiver_read` (`receiver_id`, `read_status`) USING BTREE,
KEY `idx_receiver_time` (`receiver_id`, `id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员互动消息';
+18
View File
@@ -0,0 +1,18 @@
-- 会员推送设备(极光 RegistrationID 绑定)
CREATE TABLE IF NOT EXISTS `member_push_device` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`member_id` bigint NOT NULL COMMENT '会员编号',
`registration_id` varchar(64) NOT NULL COMMENT '极光 RegistrationID',
`alias` varchar(64) NULL DEFAULT NULL COMMENT '极光别名,如 u{memberId}',
`platform` varchar(16) NOT NULL COMMENT 'ios / android',
`provider` varchar(16) NOT NULL DEFAULT 'jpush' COMMENT '推送服务商',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_registration_id` (`registration_id`) USING BTREE,
KEY `idx_member_id` (`member_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员推送设备';
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""骑遇骑行记录:建表 + 模拟数据(chat_v1 库)。可重复执行(幂等:表已存在跳过,模拟数据按标题去重)。"""
import json
import math
import pymysql
DB = dict(host='139.224.25.207', port=3306, user='chat_v1',
password='BpJ2Ebt4zMPjBcCL', database='chat_v1', charset='utf8mb4',
connect_timeout=15)
DDL = [
"""
CREATE TABLE IF NOT EXISTS `member_ride_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`user_id` bigint(20) NOT NULL COMMENT '骑手编号 member_user.id',
`title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '战报标题',
`road_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路线名(如:南昆山)',
`corner_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '弯道名(如:观音潭右弯),空表示非跑山记录',
`distance_km` decimal(8,1) NOT NULL COMMENT '里程(公里)',
`duration_sec` int(11) NOT NULL COMMENT '骑行时长(秒)',
`top_speed_kmh` decimal(5,1) NOT NULL COMMENT '极速(km/h)',
`curve_count` int(11) NOT NULL DEFAULT '0' COMMENT '过弯次数',
`max_lean` decimal(4,1) NOT NULL COMMENT '最大压弯角度(度)',
`track_json` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '轨迹点 JSON [{lat,lng,speed}],仅详情接口返回',
`like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数(冗余计数)',
`report_count` int(11) NOT NULL DEFAULT '0' COMMENT '被举报次数',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0 正常 1 已下架',
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
`creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_rank` (`road_name`, `corner_name`, `max_lean`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='骑遇骑行记录(战报)'
""",
"""
CREATE TABLE IF NOT EXISTS `member_ride_record_like` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`record_id` bigint(20) NOT NULL COMMENT '战报编号 member_ride_record.id',
`user_id` bigint(20) NOT NULL COMMENT '点赞人编号 member_user.id',
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租户编号',
`creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_record_user` (`record_id`, `user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='骑遇骑行战报点赞'
""",
]
# (user_id, title, road, corner, km, sec, top, curves, max_lean, days_ago, like_user_ids)
MOCK = [
(131, '南昆山 63 连弯,今天左弯终于不虚了', '南昆山', '观音潭右弯', 87, 5760, 138, 63, 52.0, 1, [133, 132, 134]),
(133, '清晨跑山,观音潭右弯刷出个人最佳', '南昆山', '观音潭右弯', 84, 5400, 132, 58, 49.5, 2, [131, 129]),
(129, '压弯一时爽,一直压弯一直爽', '南昆山', '观音潭右弯', 86, 5900, 129, 61, 46.0, 4, [131]),
(132, '第一次跑南昆山,右弯还是不敢深压', '南昆山', '观音潭右弯', 88, 6300, 112, 57, 33.5, 5, [134]),
(131, '连续 S 弯练车,大腿已废', '南昆山', '连续 S 弯', 52, 3900, 118, 41, 44.5, 1, [133, 129]),
(133, 'S 弯 41 连过,人车合一', '南昆山', '连续 S 弯', 52, 3950, 112, 41, 40.0, 3, [131, 134, 132]),
(129, '大夫山夜骑放风,发卡弯有点意思', '大夫山', '发卡弯', 44, 3660, 105, 38, 43.0, 1, [131, 133]),
(131, '大夫山发卡弯,极速压到 43 度', '大夫山', '发卡弯', 43, 3500, 108, 35, 43.2, 3, [133]),
(134, '傍晚放风,夕阳配弯道,舒服', '大夫山', '发卡弯', 44, 3800, 98, 36, 29.0, 4, [132]),
(131, '罗浮山风门坳长弯,全程 6 档', '罗浮山', '风门坳长弯', 121, 7680, 121, 22, 39.0, 2, [133, 129]),
(133, '风门坳长弯磨膝体验,就一次', '罗浮山', '风门坳长弯', 118, 7500, 126, 20, 47.5, 6, [131]),
(129, '周末摩旅,156 公里赶到营地', '', '', 156, 7680, 121, 22, 25.0, 3, [131, 133, 132, 134]),
(133, '市区通勤顺路兜风,绿道休闲骑', '', '', 32, 3000, 85, 12, 18.5, 1, [131]),
(132, '新手第一份战报,求带飞', '', '', 26, 2700, 76, 9, 15.0, 2, [134]),
]
def track_points(seed: int) -> str:
"""生成一个小环形轨迹 JSON(约 60 点),让战报详情有图可画。"""
r = 0.004 + (seed % 7) * 0.0004
pts = []
n = 60
for i in range(n):
t = i / n * math.pi * 4
radius = r + r * 0.35 * math.sin(t * 2.3 + seed)
lat = 23.17 + seed * 0.002 + radius * math.cos(t)
lng = 113.36 + seed * 0.003 + radius * 1.4 * math.sin(t)
speed = round(40 + 60 * (0.5 + 0.5 * math.sin(t * 1.7 + seed)), 1)
pts.append({'lat': round(lat, 6), 'lng': round(lng, 6), 'speed': speed})
return json.dumps(pts, separators=(',', ':'))
def main():
conn = pymysql.connect(**DB)
cur = conn.cursor()
for ddl in DDL:
cur.execute(ddl)
conn.commit()
print('DDL ok')
inserted = 0
for idx, (uid, title, road, corner, km, sec, top, curves, lean, days_ago, likers) in enumerate(MOCK):
cur.execute("SELECT id FROM member_ride_record WHERE title=%s AND deleted=0", (title,))
if cur.fetchone():
print('skip (exists):', title)
continue
cur.execute(
"""INSERT INTO member_ride_record
(user_id, title, road_name, corner_name, distance_km, duration_sec,
top_speed_kmh, curve_count, max_lean, track_json, like_count, report_count,
status, tenant_id, creator, create_time)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,1,%s,
DATE_SUB(NOW(), INTERVAL %s DAY) - INTERVAL %s HOUR)""",
(uid, title, road, corner, km, sec, top, curves, lean,
track_points(idx), len(likers), uid, days_ago, (idx * 3) % 20))
record_id = cur.lastrowid
for liker in likers:
cur.execute(
"""INSERT IGNORE INTO member_ride_record_like
(record_id, user_id, tenant_id, creator, create_time)
VALUES (%s,%s,1,%s, DATE_SUB(NOW(), INTERVAL 1 DAY))""",
(record_id, liker, uid))
inserted += 1
print('inserted:', title)
conn.commit()
cur.execute("SELECT COUNT(*) FROM member_ride_record WHERE deleted=0")
print('total records:', cur.fetchone()[0])
cur.execute("SELECT road_name, corner_name, COUNT(*) FROM member_ride_record "
"WHERE deleted=0 AND corner_name<>'' GROUP BY road_name, corner_name")
for r in cur.fetchall():
print('board:', r)
conn.close()
print('done, inserted =', inserted)
if __name__ == '__main__':
main()
@@ -0,0 +1,20 @@
-- Banner 位置字典:补「动态页面」= 6(App 动态顶部轮播)
-- 若库中已有 value=6,执行会因业务唯一性报错时可忽略;或先查:
-- SELECT * FROM system_dict_data WHERE dict_type='promotion_banner_position' AND value='6';
INSERT INTO `system_dict_data` (
`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`,
`remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`
)
SELECT 0, '动态页面', '6', 'promotion_banner_position', 0, 'primary', '',
'App 动态页 Banner', '1', NOW(), '1', NOW(), b'0'
FROM (SELECT 1 AS _) AS t
WHERE NOT EXISTS (
SELECT 1 FROM `system_dict_data`
WHERE `dict_type` = 'promotion_banner_position' AND `value` = '6' AND `deleted` = b'0'
);
-- 确保 Banner 菜单可见:营销中心 → 内容管理 → Banner
UPDATE `system_menu`
SET `status` = 0, `visible` = b'1', `deleted` = b'0', `update_time` = NOW()
WHERE `id` IN (2030, 2387, 2025);
@@ -0,0 +1,17 @@
-- 后台菜单与授权:子账号管理 + 会员管理拆分(男号/女号)
-- 执行库:chat_v1
-- 1) 新增菜单(幂等:先删后插)
DELETE FROM system_menu WHERE id IN (5810, 5820, 5821);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, updater, deleted)
VALUES
(5810, '子账号管理', 'system:sub-account:manage', 2, 20, 0, '/sub-account', 'ep:user', 'system/subaccount/index', 'SystemSubAccount', 0, b'1', b'1', b'1', '1', '1', b'0'),
(5820, '男号管理', 'member:user:query', 2, 1, 2262, 'male-user', 'ep:male', 'member/user/male', 'MemberUserMale', 0, b'1', b'1', b'1', '1', '1', b'0'),
(5821, '女号管理', 'member:user:query', 2, 2, 2262, 'female-user', 'ep:female', 'member/user/female', 'MemberUserFemale', 0, b'1', b'1', b'1', '1', '1', b'0');
-- 2) 授权给「APP管理员」角色(AppHolder 当前使用的角色)
DELETE FROM system_role_menu WHERE role_id = 158 AND menu_id IN (5810, 5820, 5821);
INSERT INTO system_role_menu (role_id, menu_id, creator, updater, deleted, tenant_id)
VALUES
(158, 5810, '1', '1', b'0', 1),
(158, 5820, '1', '1', b'0', 1),
(158, 5821, '1', '1', b'0', 1);
@@ -0,0 +1,22 @@
-- ----------------------------
-- 航空账号专用 OAuth2 客户端
-- 用途:airline-login 签发的 Token 归属 airline 客户端,access/refresh token 有效期极长,
-- App 端登录一次即可长期使用,无需续期;每次打开 App 仅通过 /airline-check
-- 校验账号状态/有效期/设备绑定(见 MemberAuthServiceImpl#createTokenAfterAirlineLoginSuccess)。
-- 注意:不影响 default 客户端(交友 App 会员仍为 30 分钟 accessToken + 30 天 refreshToken)。
-- 部署:执行本脚本后再部署新 yudao-server.jar,否则 airline-login 会报"客户端不存在"。
-- ----------------------------
INSERT INTO `system_oauth2_client`
(`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`,
`access_token_validity_seconds`, `refresh_token_validity_seconds`,
`redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`,
`authorities`, `resource_ids`, `additional_information`,
`creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 43, 'airline', 'airline123', '航空App专用客户端', '', '南航/东航/海航 App 专用,Token 长期有效', 0,
2147483647, 2147483647,
'[]', '["password","refresh_token"]', '[]', '[]',
'[]', '[]', '{}',
'1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM `system_oauth2_client` WHERE `client_id` = 'airline' AND `deleted` = b'0');
+17
View File
@@ -0,0 +1,17 @@
-- 子账号管理:holder 与 子账号/专属角色 的绑定关系
-- 执行位置:chat_v1 库
CREATE TABLE IF NOT EXISTS `system_sub_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`holder_user_id` bigint NOT NULL COMMENT '管理者用户编号(system_users.id)',
`sub_user_id` bigint NOT NULL COMMENT '子账号用户编号(system_users.id)',
`role_id` bigint NOT NULL COMMENT '子账号专属角色编号(system_role.id)',
`creator` varchar(64) NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
KEY `idx_holder_user_id` (`holder_user_id`),
KEY `idx_sub_user_id` (`sub_user_id`)
) ENGINE = InnoDB COMMENT '后台子账号表';
@@ -1,27 +1,80 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* 基于时间戳的 LocalDateTime 反序列化器
*
* 支持两种 JSON 写法:
* 1. 时间戳:13 位毫秒(yudao 前端默认),兼容 10 位秒级时间戳
* 2. 日期字符串:"yyyy-MM-dd HH:mm:ss"、"yyyy-MM-dd"、ISO-8601("yyyy-MM-dd'T'HH:mm:ss")
*
* 注意:原实现直接 {@code p.getValueAsLong()},传入 "yyyy-MM-dd HH:mm:ss" 字符串时
* 会静默解析失败并返回 0,导致入库时间变成 1970-01-01 08:00:00(东八区 epoch 0)。
*
* @author 老五
*/
public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer();
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 将 Long 时间戳,转换为 LocalDateTime 对象
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
// 1. 数字:直接按毫秒时间戳处理(与原行为一致)
if (p.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getLongValue()), ZoneId.systemDefault());
}
// 2. 字符串:先尝试时间戳,再尝试常见日期格式
String text = p.getValueAsString();
if (text == null) {
return null;
}
text = text.trim();
if (text.isEmpty()) {
return null;
}
// 2.1 纯数字:13 位毫秒、10 位秒
if (text.matches("\\d+")) {
long timestamp = Long.parseLong(text);
if (text.length() <= 10) { // 秒级时间戳,转毫秒
timestamp *= 1000;
}
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
}
// 2.2 "yyyy-MM-dd HH:mm:ss"
try {
return LocalDateTime.parse(text, DATE_TIME_FORMATTER);
} catch (Exception ignore) {
// 继续尝试其它格式
}
// 2.3 "yyyy-MM-dd"(按当天 00:00:00)
try {
return LocalDate.parse(text, DATE_FORMATTER).atStartOfDay();
} catch (Exception ignore) {
// 继续尝试 ISO 格式
}
// 2.4 ISO-8601:"yyyy-MM-dd'T'HH:mm:ss"
try {
return LocalDateTime.parse(text);
} catch (Exception ignore) {
// 落到最后的报错
}
throw ctxt.weirdStringException(text, LocalDateTime.class,
"Expected timestamp (millis or seconds) or date string (yyyy-MM-dd HH:mm:ss)");
}
}
@@ -387,6 +387,12 @@ public class GlobalExceptionHandler {
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
}
// 7.1 会员互动消息
if (message.contains("member_interaction")) {
log.error("[会员互动消息表 member_interaction 未导入][请执行 friend_back/sql/mysql/member/member_interaction.sql]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"会员互动消息表未导入,请执行 sql/mysql/member/member_interaction.sql");
}
// 8. AI 大模型
if (message.contains("ai_")) {
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
@@ -30,6 +30,14 @@ public interface TradeBrokerageRecordApi {
*/
void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, @Valid List<BrokerageAddDTO> list);
/**
* 取消佣金:将佣金记录状态修改为已失效
*
* @param bizType 业务类型
* @param bizId 业务编号
*/
void cancelBrokerage(BrokerageRecordBizTypeEnum bizType, String bizId);
/**
* 微信转账
* @param id 提现单id
@@ -16,8 +16,9 @@ import java.util.Arrays;
public enum BrokerageRecordBizTypeEnum implements ArrayValuable<Integer> {
ORDER(1, "获得推广佣金", "获得推广佣金 {}", true),
WITHDRAW(2, "提现申请", "提现申请扣除佣金 {}", false),
WITHDRAW_REJECT(3, "提现申请驳回", "提现申请驳回,返还佣金 {}", true),
OPEN_TYPE(2, "获得会员佣金", "获得会员佣金 {}", true),
WITHDRAW(3, "提现申请", "提现申请扣除佣金 {}", false),
WITHDRAW_REJECT(4, "提现申请驳回", "提现申请驳回,返还佣金 {}", true),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageRecordBizTypeEnum::getType).toArray(Integer[]::new);
@@ -18,8 +18,12 @@ public enum BrokerageWithdrawTypeEnum implements ArrayValuable<Integer> {
WALLET(1, "钱包"),
BANK(2, "银行卡"),
WECHAT(3, "微信"), // 手动打款
ALIPAY(4, "支付宝"),
WECHAT_API(5, "微信零钱"), // 自动打款,通过微信转账 API
ALIPAY(4, "支付宝"), // 手动打款
/**
* 历史命名 WECHAT_API;App 端佣金提现实际传 type=5 + 支付宝账号,运营后台手动打款。
* 切勿再走微信转账 API。
*/
WECHAT_API(5, "支付宝"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BrokerageWithdrawTypeEnum::getType).toArray(Integer[]::new);
@@ -39,13 +43,11 @@ public enum BrokerageWithdrawTypeEnum implements ArrayValuable<Integer> {
}
/**
* 是否通过支付平台的 API 打款
*
* @param type 类型
* @return 是否
* 是否通过支付平台的 API 打款。
* 当前业务均为运营手动打款,不再走微信/支付宝自动转账。
*/
public static boolean isApi(Integer type) {
return WECHAT_API.getType().equals(type);
return false;
}
}
@@ -63,6 +63,11 @@ public class TradeBrokerageRecordApiImpl implements TradeBrokerageRecordApi {
brokerageRecordService.addBrokerage(userId, bizType, BeanUtils.toBean(list, BrokerageAddReqBO.class));
}
@Override
public void cancelBrokerage(BrokerageRecordBizTypeEnum bizType, String bizId) {
brokerageRecordService.cancelBrokerage(bizType, bizId);
}
/**
* 微信转账
*
@@ -1,10 +1,15 @@
package cn.iocoder.yudao.module.trade.controller.admin.brokerage;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayTransferNotifyReqDTO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawExcelVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRejectReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO;
@@ -22,9 +27,15 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@@ -81,6 +92,39 @@ public class BrokerageWithdrawController {
return success(BrokerageWithdrawConvert.INSTANCE.convertPage(pageResult, userMap));
}
@GetMapping("/export-excel")
@Operation(summary = "导出佣金提现 Excel")
@PreAuthorize("@ss.hasPermission('trade:brokerage-withdraw:query')")
@ApiAccessLog(operateType = EXPORT)
public void exportBrokerageWithdrawExcel(@Valid BrokerageWithdrawPageReqVO pageVO,
HttpServletResponse response) throws IOException {
pageVO.setPageSize(PageParam.PAGE_SIZE_NONE);
PageResult<BrokerageWithdrawDO> pageResult = brokerageWithdrawService.getBrokerageWithdrawPage(pageVO);
Map<Long, MemberUserRespDTO> userMap = memberUserApi.getUserMap(
convertSet(pageResult.getList(), BrokerageWithdrawDO::getUserId));
List<BrokerageWithdrawExcelVO> excelList = new ArrayList<>(pageResult.getList().size());
for (BrokerageWithdrawDO row : pageResult.getList()) {
BrokerageWithdrawExcelVO excel = new BrokerageWithdrawExcelVO();
excel.setId(row.getId());
excel.setUserId(row.getUserId());
excel.setUserNickname(Optional.ofNullable(userMap.get(row.getUserId()))
.map(MemberUserRespDTO::getNickname).orElse(null));
excel.setPriceYuan(row.getPrice() == null ? "0" : MoneyUtils.fenToYuanStr(row.getPrice()));
excel.setFeePriceYuan(row.getFeePrice() == null ? "0" : MoneyUtils.fenToYuanStr(row.getFeePrice()));
excel.setTypeName("支付宝");
excel.setAccountNo(row.getAccountNo());
excel.setName(row.getName());
excel.setStatus(row.getStatus());
excel.setAuditReason(row.getAuditReason());
excel.setAuditTime(row.getAuditTime());
excel.setCreateTime(row.getCreateTime());
excel.setRemark(row.getRemark());
excelList.add(excel);
}
ExcelUtils.write(response, "佣金提现.xls", "数据", BrokerageWithdrawExcelVO.class, excelList);
}
// TODO @luchi:update-transferred,url 改成这个。和 update-paid 、update-refunded 保持一致
@PostMapping("/update-transfer")
@Operation(summary = "更新转账订单为转账成功") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.trade.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 佣金提现 Excel VO
*/
@Data
public class BrokerageWithdrawExcelVO {
@ExcelProperty("编号")
private Long id;
@ExcelProperty("用户编号")
private Long userId;
@ExcelProperty("用户昵称")
private String userNickname;
@ExcelProperty("提现金额(元)")
private String priceYuan;
@ExcelProperty("手续费(元)")
private String feePriceYuan;
@ExcelProperty("提现方式")
private String typeName;
@ExcelProperty("支付宝账号")
private String accountNo;
@ExcelProperty("真实姓名")
private String name;
@ExcelProperty(value = "状态", converter = DictConvert.class)
@DictFormat(DictTypeConstants.BROKERAGE_WITHDRAW_STATUS)
private Integer status;
@ExcelProperty("审核原因")
private String auditReason;
@ExcelProperty("审核时间")
private LocalDateTime auditTime;
@ExcelProperty("申请时间")
private LocalDateTime createTime;
@ExcelProperty("备注")
private String remark;
}
@@ -30,6 +30,15 @@ public class AppBrokerageWithdrawRespVO {
@Schema(description = "提现时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "审核驳回原因", example = "信息不完整")
private String auditReason;
@Schema(description = "转账失败原因", example = "用户未确认收款")
private String transferErrorMsg;
@Schema(description = "备注", example = "账号信息有误")
private String remark;
// ========== 微信转账专属 ==========
@Schema(description = "转账单编号", example = "1024")
@@ -38,7 +38,8 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_C
@Validated
public class BrokerageCodeServiceImpl implements BrokerageCodeService {
private static final String POSTER_VERSION_MARKER = "#poster-v2-hd";
/** v4:邀请链接改为 /invite.html?code=&userId=,二维码同步刷新 */
private static final String POSTER_VERSION_MARKER = "#poster-v4-invite-html";
private static final String BROKERAGE_CODE_GENERATE_LOCK_KEY = "trade:brokerage-code:generate:";
@@ -144,17 +145,14 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
if (ObjectUtil.isEmpty(currentCode) || isLatestPoster(currentCode.getPosterPic())) {
return currentCode;
}
// 海报图统一印分销码 code,与手动填推荐码查表字段一致
String posterCode = currentCode.getCode();
String userAvatar = "";
MemberUserRespDTO user = memberUserApi.getUser(userId);
if (ObjectUtil.isNotEmpty(user)) {
userAvatar = user.getAvatar();
if (ObjectUtil.isNotEmpty(user.getUserCode())) {
posterCode = user.getUserCode();
}
}
String posterUrl = ObjectUtil.isNotEmpty(currentCode.getUrl())
? currentCode.getUrl() : generatePosterUrl(currentCode.getCode(), userId);
String posterUrl = generatePosterUrl(currentCode.getCode(), userId);
String posterPic = generatePosterPic(userAvatar, posterCode, posterUrl);
if (ObjectUtil.isEmpty(posterPic)) {
return currentCode;
@@ -204,19 +202,15 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
isRepet = ObjectUtil.isNotEmpty(brokerageCode);
}
// 查询用户获取用户头像
// 查询用户获取用户头像;海报图印分销码 code(与绑定查表一致)
String userAvatar = "";
String posterCode = code;
MemberUserRespDTO user = memberUserApi.getUser(userId);
if (ObjectUtil.isNotEmpty(user)) {
userAvatar = user.getAvatar();
if (ObjectUtil.isNotEmpty(user.getUserCode())) {
posterCode = user.getUserCode();
}
}
String url = generatePosterUrl(code, userId);
BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(userAvatar, posterCode, url)).url(url).build();
BrokerageCodeDO saveObj = BrokerageCodeDO.builder().memberId(userId).code(code).posterPic(generatePosterPic(userAvatar, code, url)).url(url).build();
brokerageCodeMapper.insert(saveObj);
return saveObj;
}
@@ -237,7 +231,12 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
}
private String generatePosterUrl(String code, Long userId) {
return brokerageConfiguration.getDownloadUrl() + "?code=" + code + "&userId=" + userId;
String base = brokerageConfiguration.getDownloadUrl();
if (base != null && base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
// 格式:http://download.htysapp.com/invite.html?code=569876&userId=90
return base + "/invite.html?code=" + code + "&userId=" + userId;
}
}
@@ -79,14 +79,10 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, List<BrokerageAddReqBO> list) {
TradeConfigDO memberConfig = tradeConfigService.getTradeConfig();
// 0 未启用分销功能
if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) {
log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list);
return;
}
// 1.1 获得一级推广人
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
if (firstUser == null) {
return;
}
// 1.2 计算一级分佣
@@ -98,7 +94,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
if (secondUser == null) {
return;
}
// 2.2 计算二级分佣
@@ -124,9 +120,9 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
// 1.1 获得一级推广人
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
if (firstUser == null) {
return;
}
// 1.1.1查看一级分销员是否已经分销过
@@ -145,7 +141,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return;
}
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
if (secondUser == null) {
return;
}
// 2.1.1查看二级分销员是否已经分销过
@@ -189,14 +189,9 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
}
// 2.3 校验能否绑定
validateCanBindUser(brokerageUser, bindUserId);
// 2.3 绑定用户
// 2.3 绑定用户(人人分销:新用户默认具备分销资格)
if (isNewBrokerageUser) {
Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition();
if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
} else {
brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now());
}
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
} else {
brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId)));
@@ -124,17 +124,14 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
.setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
.setBizType(PayWalletBizTypeEnum.BROKERAGE_WITHDRAW.getType()).setBizId(withdraw.getId().toString())
.setPrice(withdraw.getPrice()));
// 1.2 微信 API
} else if (BrokerageWithdrawTypeEnum.WECHAT_API.getType().equals(withdraw.getType())) {
// TODO @luchi:这里,要加个转账单号的记录;另外,调用 API 转账,是立马成功,还是有延迟的哈?
createPayTransfer(withdraw);
// 1.3 剩余类型,都是手动打款,所以不处理
// 1.2 历史 type=5 曾走微信转账 API;现业务为支付宝账号提现,统一手动打款
// 若再走 createPayTransfer,无小程序 openid 会 NPE → 管理端「系统异常」
} else {
// TODO 可优化:未来可以考虑,接入支付宝、银联等 API 转账,实现自动打款
log.info("[auditBrokerageWithdrawSuccess][withdraw({}) 类型({}) 手动打款,无需处理]", withdraw.getId(), withdraw.getType());
log.info("[auditBrokerageWithdrawSuccess][withdraw({}) 类型({}) 手动打款,账号={}]",
withdraw.getId(), withdraw.getType(), withdraw.getAccountNo());
}
// 2. 非支付 API,则直接体现成功
// 2. 非支付 API,则直接提现成功
if (!BrokerageWithdrawTypeEnum.isApi(withdraw.getType())) {
brokerageWithdrawMapper.updateByIdAndStatus(withdraw.getId(), BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.getStatus(),
new BrokerageWithdrawDO().setStatus(BrokerageWithdrawStatusEnum.WITHDRAW_SUCCESS.getStatus()));
@@ -55,7 +55,7 @@ public class PosterPicGen {
private int pageHeight = 700;
private String title = "荷塘月色";
private String title = "荷塘助手";
private String description = "荷塘湖畔 相伴月色";
@@ -160,10 +160,8 @@ public class PosterPicGen {
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
}
private void drawUserAvatar(BufferedImage avatar) {
if (avatar == null) {
return;
}
private void drawUserAvatar(BufferedImage avatar) throws IOException{
BufferedImage logoAvater = ImageIO.read(ResourceUtil.getResource("image/logo.png"));
graphics.setColor(Color.WHITE);
graphics.fill(new RoundRectangle2D.Double(
AVATAR_X - AVATAR_CLEAR_MARGIN,
@@ -172,7 +170,7 @@ public class PosterPicGen {
AVATAR_SIZE + AVATAR_CLEAR_MARGIN * 2.0,
(AVATAR_CORNER_RADIUS + AVATAR_CLEAR_MARGIN) * 2.0,
(AVATAR_CORNER_RADIUS + AVATAR_CLEAR_MARGIN) * 2.0));
drawAvatar(avatar, AVATAR_X, AVATAR_Y, AVATAR_SIZE,
drawAvatar(logoAvater, AVATAR_X, AVATAR_Y, AVATAR_SIZE,
new RoundRectangle2D.Double(0, 0, AVATAR_SIZE, AVATAR_SIZE,
AVATAR_CORNER_RADIUS * 2.0, AVATAR_CORNER_RADIUS * 2.0));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

@@ -91,4 +91,24 @@ public interface MemberErrorCode {
ErrorCode CHAT_USER_COUNT_EXCEED = new ErrorCode(1_004_018_000, "当天免费聊天人数已用完");
ErrorCode CHAT_MSG_COUNT_EXCEED = new ErrorCode(1_004_018_001, "当天免费聊天次数已用完");
ErrorCode CHAT_TOKEN_CREATE_FAILED = new ErrorCode(1_004_018_002, "聊天登录凭证签发失败,请稍后重试");
//========== 航空账号 1-004-019-000 ==========
ErrorCode AIRLINE_ACCOUNT_NOT_EXISTS = new ErrorCode(1_004_019_000, "航空账号不存在");
ErrorCode AIRLINE_ACCOUNT_MOBILE_EXISTS = new ErrorCode(1_004_019_001, "该手机号已存在航空账号");
ErrorCode AIRLINE_ACCOUNT_DISABLED = new ErrorCode(1_004_019_002, "航空账号已禁用");
ErrorCode AIRLINE_ACCOUNT_NOT_STARTED = new ErrorCode(1_004_019_003, "航空账号尚未生效");
ErrorCode AIRLINE_ACCOUNT_EXPIRED = new ErrorCode(1_004_019_004, "航空账号已到期,无法登录");
ErrorCode AIRLINE_ACCOUNT_DEVICE_BOUND = new ErrorCode(1_004_019_005, "该账号已绑定其他手机,请联系管理员解绑");
ErrorCode AIRLINE_ACCOUNT_DEVICE_USED = new ErrorCode(1_004_019_006, "该手机已绑定其他航空账号");
ErrorCode AIRLINE_ACCOUNT_TIME_INVALID = new ErrorCode(1_004_019_007, "结束时间必须晚于开始时间");
ErrorCode AIRLINE_ACCOUNT_BAD_CREDENTIALS = new ErrorCode(1_004_019_008, "航空账号手机号或密码不正确");
// ========== 骑遇发车局 / 骑行标签 1-004-020-000 ==========
ErrorCode RIDE_PLAN_NOT_EXISTS = new ErrorCode(1_004_020_000, "发车局不存在或已下架");
ErrorCode RIDE_PLAN_MEET_TIME_INVALID = new ErrorCode(1_004_020_001, "集合时间必须晚于当前时间");
ErrorCode RIDE_PLAN_FULL = new ErrorCode(1_004_020_002, "该发车局已满员");
ErrorCode RIDE_PLAN_ALREADY_JOINED = new ErrorCode(1_004_020_003, "已加入该发车局,请勿重复加入");
ErrorCode RIDE_PLAN_NOT_JOINED = new ErrorCode(1_004_020_004, "尚未加入该发车局");
ErrorCode RIDE_TAG_NOT_EXISTS = new ErrorCode(1_004_020_005, "骑行标签不存在或已停用");
ErrorCode RIDE_RECORD_NOT_EXISTS = new ErrorCode(1_004_020_006, "骑行战报不存在或已下架");
}
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.member.enums.interaction;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 互动消息类型
*/
@Getter
@AllArgsConstructor
public enum MemberInteractionBizTypeEnum {
VIEWED("VIEWED", "查看了你"),
WECHAT_UNLOCKED("WECHAT_UNLOCKED", "解锁了你的微信");
private final String type;
private final String contentSuffix;
}
@@ -43,6 +43,11 @@
<artifactId>yudao-module-pay-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-trade-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
@@ -101,6 +106,13 @@
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId>
</dependency>
<!-- 极光推送 -->
<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jpush-client</artifactId>
<version>3.7.7</version>
</dependency>
<!-- 文字、图片风险认证 -->
<dependency>
<groupId>com.aliyun</groupId>
@@ -17,7 +17,7 @@ public final class ChatConstant {
* 客服用户id
*/
public static final Long COM_SERVICE_USER_ID = -200L;
public static final String COM_SERVICE_USER_NAME = "荷塘月色客服";
public static final String COM_SERVICE_USER_NAME = "荷塘助手客服";
public static final String COM_SERVICE_USER_AVATAR = "https://api.htysapp.com/admin-api/infra/file/4/get/affa4cac924cb13fe5706100828685015d512bca7dfa0e26ef82422b31877abc.png";
public static final List<Long> IGNORE_USER_IDS = Arrays.asList(0L, COM_MESSAGE_USER_ID, COM_SERVICE_USER_ID);
@@ -29,6 +29,11 @@ public interface ChatApi {
*/
boolean createOrUpdate(@Valid ChatCreateOrUpdateUserReqVO dto);
/**
* 封禁聊天用户并推送下线
*/
boolean banUser(Long userId, String reason);
/**
* 根据userId查询用户详情
*/
@@ -121,6 +121,36 @@ public class ChatApiImpl implements ChatApi {
}
}
/**
* 封禁聊天用户并推送下线
*/
@Override
public boolean banUser(Long userId, String reason) {
if (userId == null) {
return false;
}
ChatApiTool.API api = setApiConfig(ChatApiTool.USER_BAN);
try {
ChatBanUserReqVO dto = new ChatBanUserReqVO(userId, reason);
String body = JSONUtil.toJsonStr(dto);
String res = HttpRequest.post(api.getFullPath()).header(apiCountersign(body)).body(body).execute().body();
log.debug("封禁聊天用户,res:{}", res);
if (ObjectUtil.isEmpty(res)) {
log.error("封禁聊天用户,res为空");
return false;
}
ChatBaseRespVO baseRes = JSONUtil.toBean(res, ChatBaseRespVO.class);
if (ObjectUtil.isEmpty(baseRes) || !baseRes.isSuccess()) {
log.error("封禁聊天用户,返回错误,res:{}", res);
return false;
}
return true;
} catch (Exception e) {
log.error("封禁聊天用户,意外的错误,userId={}", userId, e);
return false;
}
}
/**
* 根据userId查询用户详情
*/
@@ -10,6 +10,7 @@ import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -42,6 +43,8 @@ final class FaceVerifyImageProcessor {
private static final int MAX_WIDTH = 1080;
/** 阿里云允许的图片最大高度 */
private static final int MAX_HEIGHT = 1920;
/** 横图转竖图时使用的目标宽高比(宽:高),约等于 3:4 */
private static final double PORTRAIT_ASPECT = 3D / 4D;
/** JPEG 输出质量:0.9,用于兼顾人脸清晰度和文件大小 */
private static final float JPEG_QUALITY = 0.90F;
@@ -85,9 +88,26 @@ final class FaceVerifyImageProcessor {
throw new IllegalArgumentException("人脸认证图片格式不支持或文件已损坏");
}
// iPhone 竖拍常是「横像素 + EXIF Orientation」,必须先转正再判断竖/横图
int orientation = readJpegExifOrientation(sourceImage);
if (orientation > 1) {
BufferedImage oriented = applyExifOrientation(decodedImage, orientation);
decodedImage.flush();
decodedImage = oriented;
log.info("|人脸识别|,已按 EXIF Orientation={} 转正比对图", orientation);
}
int sourceWidth = decodedImage.getWidth();
int sourceHeight = decodedImage.getHeight();
// 阿里云要求人脸比对照片为竖图;横图不做裁剪,避免裁掉人脸区域。
// 阿里云要求人脸比对照片为竖图(高>宽);横图居中裁成竖图,避免直接失败
if (sourceHeight <= sourceWidth) {
BufferedImage portrait = cropToPortrait(decodedImage);
decodedImage.flush();
decodedImage = portrait;
sourceWidth = decodedImage.getWidth();
sourceHeight = decodedImage.getHeight();
log.info("|人脸识别|,横图已居中裁成竖图,尺寸={}x{}", sourceWidth, sourceHeight);
}
if (sourceHeight <= sourceWidth) {
decodedImage.flush();
throw new IllegalArgumentException("人脸认证图片必须为竖图,且高度大于宽度");
@@ -113,11 +133,175 @@ final class FaceVerifyImageProcessor {
encodedImage = encodeJpeg(processedImage);
}
log.info("|人脸识别|,图片尺寸处理完成,原始尺寸={}x{},处理后尺寸={}x{}", sourceWidth, sourceHeight, processedImage.getWidth(), processedImage.getHeight());
log.info("|人脸识别|,图片尺寸处理完成,原始尺寸={}x{},处理后尺寸={}x{}",
sourceWidth, sourceHeight, processedImage.getWidth(), processedImage.getHeight());
processedImage.flush();
return encodedImage;
}
/**
* 将横图居中裁成竖图(约 3:4),尽量保留中间人脸区域。
*/
private static BufferedImage cropToPortrait(BufferedImage source) {
int width = source.getWidth();
int height = source.getHeight();
int cropHeight = height;
int cropWidth = (int) Math.floor(cropHeight * PORTRAIT_ASPECT);
if (cropWidth >= cropHeight) {
cropWidth = Math.max(1, cropHeight - 1);
}
if (cropWidth > width) {
cropWidth = width;
cropHeight = (int) Math.floor(cropWidth / PORTRAIT_ASPECT);
if (cropHeight > height) {
cropHeight = height;
}
if (cropHeight <= cropWidth) {
cropHeight = Math.min(height, cropWidth + 1);
}
}
int x = Math.max(0, (width - cropWidth) / 2);
int y = Math.max(0, (height - cropHeight) / 2);
cropWidth = Math.min(cropWidth, width - x);
cropHeight = Math.min(cropHeight, height - y);
return source.getSubimage(x, y, cropWidth, cropHeight);
}
/**
* 解析 JPEG EXIF Orientation(Tag 0x0112),非 JPEG / 失败返回 1。
*/
private static int readJpegExifOrientation(byte[] jpeg) {
try {
if (jpeg == null || jpeg.length < 4 || (jpeg[0] & 0xFF) != 0xFF || (jpeg[1] & 0xFF) != 0xD8) {
return 1;
}
int offset = 2;
while (offset + 4 < jpeg.length) {
if ((jpeg[offset] & 0xFF) != 0xFF) {
break;
}
int marker = jpeg[offset + 1] & 0xFF;
offset += 2;
if (marker == 0xD9 || marker == 0xDA) {
break;
}
if (offset + 2 > jpeg.length) {
break;
}
int segLen = ((jpeg[offset] & 0xFF) << 8) | (jpeg[offset + 1] & 0xFF);
if (segLen < 2 || offset + segLen > jpeg.length) {
break;
}
if (marker == 0xE1 && segLen >= 8) {
int exifStart = offset + 2;
if (exifStart + 6 <= jpeg.length
&& jpeg[exifStart] == 'E' && jpeg[exifStart + 1] == 'x'
&& jpeg[exifStart + 2] == 'i' && jpeg[exifStart + 3] == 'f'
&& jpeg[exifStart + 4] == 0 && jpeg[exifStart + 5] == 0) {
int tiff = exifStart + 6;
boolean little = jpeg[tiff] == 'I' && jpeg[tiff + 1] == 'I';
if (!(little || (jpeg[tiff] == 'M' && jpeg[tiff + 1] == 'M'))) {
return 1;
}
int ifd0 = tiff + readInt(jpeg, tiff + 4, 4, little);
if (ifd0 + 2 > jpeg.length) {
return 1;
}
int entries = readInt(jpeg, ifd0, 2, little);
for (int i = 0; i < entries; i++) {
int entry = ifd0 + 2 + i * 12;
if (entry + 12 > jpeg.length) {
break;
}
int tag = readInt(jpeg, entry, 2, little);
if (tag == 0x0112) {
return readInt(jpeg, entry + 8, 2, little);
}
}
}
}
offset += segLen;
}
} catch (Exception ignore) {
// ignore
}
return 1;
}
private static int readInt(byte[] data, int off, int len, boolean littleEndian) {
int v = 0;
if (littleEndian) {
for (int i = 0; i < len; i++) {
v |= (data[off + i] & 0xFF) << (8 * i);
}
} else {
for (int i = 0; i < len; i++) {
v = (v << 8) | (data[off + i] & 0xFF);
}
}
return v;
}
private static BufferedImage applyExifOrientation(BufferedImage src, int orientation) {
int w = src.getWidth();
int h = src.getHeight();
AffineTransform tx = new AffineTransform();
int newW = w;
int newH = h;
switch (orientation) {
case 2:
tx.scale(-1, 1);
tx.translate(-w, 0);
break;
case 3:
tx.translate(w, h);
tx.rotate(Math.PI);
break;
case 4:
tx.scale(1, -1);
tx.translate(0, -h);
break;
case 5:
tx.rotate(Math.PI / 2);
tx.scale(-1, 1);
newW = h;
newH = w;
break;
case 6: // 90 CW —— iPhone 竖拍最常见
tx.translate(h, 0);
tx.rotate(Math.PI / 2);
newW = h;
newH = w;
break;
case 7:
tx.scale(-1, 1);
tx.translate(-h, 0);
tx.translate(0, w);
tx.rotate(3 * Math.PI / 2);
newW = h;
newH = w;
break;
case 8:
tx.translate(0, w);
tx.rotate(3 * Math.PI / 2);
newW = h;
newH = w;
break;
default:
return src;
}
BufferedImage dst = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB);
Graphics2D g = dst.createGraphics();
try {
g.setColor(Color.WHITE);
g.fillRect(0, 0, newW, newH);
g.drawImage(src, tx, null);
} finally {
g.dispose();
}
return dst;
}
/**
* 根据阿里云尺寸限制计算目标宽高,保持图片原始宽高比。
* 大图优先将宽度缩放至 720 像素,小图仅放大至接口要求的最小尺寸。
@@ -133,7 +317,8 @@ final class FaceVerifyImageProcessor {
int targetWidth = Math.max(1, (int) Math.floor(sourceWidth * scale));
int targetHeight = Math.max(1, (int) Math.floor(sourceHeight * scale));
if (targetWidth < MIN_WIDTH || targetHeight < MIN_HEIGHT || targetWidth > MAX_WIDTH || targetHeight > MAX_HEIGHT) {
throw new IllegalArgumentException(String.format("人脸认证图片比例无法满足阿里云尺寸要求,原始尺寸:%dx%d", sourceWidth, sourceHeight));
throw new IllegalArgumentException(String.format(
"人脸认证图片比例无法满足阿里云尺寸要求,原始尺寸:%dx%d", sourceWidth, sourceHeight));
}
return new int[]{targetWidth, targetHeight};
}
@@ -216,7 +401,6 @@ final class FaceVerifyImageProcessor {
throw new IllegalArgumentException("人脸认证原始图片不能超过10MB");
}
// 即使服务端未返回 Content-Length,也在读取过程中限制总字节数,避免占用过多内存。
try (InputStream inputStream = connection.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(
contentLength > 0 ? Math.min(contentLength, MAX_SOURCE_IMAGE_BYTES) : 8192)) {
@@ -2,6 +2,9 @@ package cn.iocoder.yudao.module.member.client.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.member.client.vo.thirdfaceverify.ThirdFindFaceVerifyReqVo;
import cn.iocoder.yudao.module.member.client.vo.thirdfaceverify.ThirdFindFaceVerifyResVo;
import cn.iocoder.yudao.module.member.client.vo.thirdfaceverify.ThirdFaceVerifyReqVO;
@@ -56,8 +59,23 @@ public class ThirdFaceVerifyServiceImpl implements ThirdFaceVerifyService {
// 参数
request.setOuterOrderNo(dto.getCode());
request.setUserId(String.valueOf(dto.getFaceVerifyUserId()));
request.setMetaInfo(dto.getMetaInfoJson());
// request.setFaceContrastPictureUrl(dto.getFaceVerifyUrl());
// MetaInfo 必须是客户端 getMetaInfos 原始字符串;错误包装会导致 SDK 不弹刷脸页
String metaInfo = dto.getMetaInfoJson();
if (StrUtil.isNotBlank(metaInfo) && metaInfo.trim().startsWith("{") && metaInfo.contains("\"metaInfoJson\"")) {
try {
JSONObject obj = JSONUtil.parseObj(metaInfo);
String nested = obj.getStr("metaInfoJson");
if (StrUtil.isNotBlank(nested)) {
log.warn("|人脸识别|,检测到 MetaInfo 被包了一层 authExtraJson,已自动解开");
metaInfo = nested;
}
} catch (Exception ignore) {
// keep original
}
}
request.setMetaInfo(metaInfo);
log.info("|人脸识别|,InitFaceVerify outerOrderNo={} userId={} metaLen={}",
dto.getCode(), dto.getFaceVerifyUserId(), metaInfo == null ? 0 : metaInfo.length());
request.setFaceContrastPicture(base64Image);
// 配置
@@ -137,16 +155,58 @@ public class ThirdFaceVerifyServiceImpl implements ThirdFaceVerifyService {
log.error("|人脸识别|,第三方接口无响应");
return null;
}
ThirdFindFaceVerifyResVo result = new ThirdFindFaceVerifyResVo();
DescribeFaceVerifyResponseBody body = response.getBody();
if (ObjectUtil.notEqual(body.getCode(), "200")) {
result.setErrorMessage(body.getMessage());
}
ThirdFindFaceVerifyResVo result = new ThirdFindFaceVerifyResVo();
DescribeFaceVerifyResponseBody.DescribeFaceVerifyResponseBodyResultObject resultObject = body.getResultObject();
result = BeanUtil.toBean(resultObject, ThirdFindFaceVerifyResVo.class);
if (ObjectUtil.isNotEmpty(resultObject)) {
result = BeanUtil.toBean(resultObject, ThirdFindFaceVerifyResVo.class);
if (result == null) {
result = new ThirdFindFaceVerifyResVo();
}
}
if (ObjectUtil.notEqual(body.getCode(), "200")) {
// 接口级失败信息(不可被 ResultObject 拷贝覆盖)
result.setErrorMessage(body.getMessage());
log.warn("|人脸识别|,DescribeFaceVerify 失败 code={} msg={} certifyId={}",
body.getCode(), body.getMessage(), code.getCertifyId());
} else if (ObjectUtil.isEmpty(result.getErrorMessage())) {
result.setErrorMessage(describeSubCode(result.getSubCode(), result.getPassed()));
}
log.info("|人脸识别|,DescribeFaceVerify certifyId={} passed={} subCode={} err={}",
code.getCertifyId(), result.getPassed(), result.getSubCode(), result.getErrorMessage());
return result;
}
/** 阿里云 ResultObject.SubCode → 可读原因(对齐官方错误码说明) */
private static String describeSubCode(String subCode, String passed) {
if (ObjectUtil.equals("T", passed) || ObjectUtil.equals("200", subCode)) {
return "认证通过";
}
if (ObjectUtil.isEmpty(subCode)) {
return ObjectUtil.equals("F", passed) ? "认证未通过" : null;
}
switch (subCode) {
case "201":
return "姓名和身份证不一致";
case "202":
return "查询不到身份信息";
case "203":
return "查询不到照片或照片不可用";
case "204":
return "人脸比对不一致(与上传头像不是同一人或活体质量偏低)";
case "205":
return "活体检测存在风险";
case "206":
return "业务策略限制(设备环境异常/高频认证等)";
case "207":
return "人脸与身份证人脸比对不一致";
case "209":
return "权威比对源异常";
default:
return "认证未通过(subCode=" + subCode + ")";
}
}
private DescribeFaceVerifyResponse describeFaceVerifyAutoRoute(DescribeFaceVerifyRequest request) {
DescribeFaceVerifyResponse lastResponse = null;
for (int i = 0; i < endpoints.size(); i++) {
@@ -21,6 +21,7 @@ public class ChatApiTool {
public static final API USER_LOGIN = API.builder().summary("用户登录").url("/third/user/login").method(HttpMethod.POST).build();
public static final API USER_GET = API.builder().summary("根据userId查询用户详情").url("/third/user/getUserByUserId").method(HttpMethod.POST).build();
public static final API USER_SAVE_OR_UPDATE = API.builder().summary("创建或者更新用户").url("/third/user/createOrUpdate").method(HttpMethod.POST).build();
public static final API USER_BAN = API.builder().summary("封禁用户并踢下线").url("/third/user/ban").method(HttpMethod.POST).build();
public static final API GET_USER_ONLINE_STATUS_BY_ID = API.builder().summary("查询用户在线状态").url("/third/user/getUserOnlineStatusById").method(HttpMethod.POST).build();
// 朋友圈
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.member.client.vo.chat.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "封禁聊天用户")
public class ChatBanUserReqVO {
@NotNull(message = "用户id不可为空")
@Schema(description = "用户id")
private Long userId;
@Schema(description = "封禁原因")
private String reason;
}
@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.member.controller.admin.airline;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.controller.admin.airline.vo.*;
import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO;
import cn.iocoder.yudao.module.member.service.airline.MemberAirlineAccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 航空账号")
@RestController
@RequestMapping("/member/airline-account")
@Validated
public class MemberAirlineAccountController {
@Resource
private MemberAirlineAccountService airlineAccountService;
@PostMapping("/create")
@Operation(summary = "创建航空账号")
@PreAuthorize("@ss.hasPermission('member:airline-account:create')")
public CommonResult<Long> createAirlineAccount(@Valid @RequestBody MemberAirlineAccountCreateReqVO createReqVO) {
return success(airlineAccountService.createAirlineAccount(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新航空账号")
@PreAuthorize("@ss.hasPermission('member:airline-account:update')")
public CommonResult<Boolean> updateAirlineAccount(@Valid @RequestBody MemberAirlineAccountUpdateReqVO updateReqVO) {
airlineAccountService.updateAirlineAccount(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除航空账号")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('member:airline-account:delete')")
public CommonResult<Boolean> deleteAirlineAccount(@RequestParam("id") Long id) {
airlineAccountService.deleteAirlineAccount(id);
return success(true);
}
@PutMapping("/unbind-device")
@Operation(summary = "解绑设备(换机后需重新登录绑定)")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('member:airline-account:unbind')")
public CommonResult<Boolean> unbindDevice(@RequestParam("id") Long id) {
airlineAccountService.unbindDevice(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得航空账号")
@Parameter(name = "id", description = "编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('member:airline-account:query')")
public CommonResult<MemberAirlineAccountRespVO> getAirlineAccount(@RequestParam("id") Long id) {
return success(convert(airlineAccountService.getAirlineAccount(id)));
}
@GetMapping("/page")
@Operation(summary = "获得航空账号分页")
@PreAuthorize("@ss.hasPermission('member:airline-account:query')")
public CommonResult<PageResult<MemberAirlineAccountRespVO>> getAirlineAccountPage(
@Valid MemberAirlineAccountPageReqVO pageReqVO) {
PageResult<MemberAirlineAccountDO> pageResult = airlineAccountService.getAirlineAccountPage(pageReqVO);
List<MemberAirlineAccountRespVO> list = pageResult.getList().stream()
.map(this::convert).collect(Collectors.toList());
return success(new PageResult<>(list, pageResult.getTotal()));
}
private MemberAirlineAccountRespVO convert(MemberAirlineAccountDO account) {
if (account == null) {
return null;
}
MemberAirlineAccountRespVO vo = BeanUtils.toBean(account, MemberAirlineAccountRespVO.class);
LocalDateTime now = LocalDateTime.now();
vo.setExpired(account.getEndTime() != null && now.isAfter(account.getEndTime()));
vo.setNotStarted(account.getStartTime() != null && now.isBefore(account.getStartTime()));
return vo;
}
}
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.member.controller.admin.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 航空账号新增/修改 Base VO")
@Data
public class MemberAirlineAccountBaseVO {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000")
@NotEmpty(message = "手机号不能为空")
@Length(min = 11, max = 11, message = "手机号长度为 11 位")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
@NotEmpty(message = "品牌渠道不能为空")
@Pattern(regexp = "CZ|MU|HU", message = "品牌渠道仅支持 CZ(南航)/ MU(东航)/ HU(海航)")
private String channel;
@Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "开始时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime startTime;
@Schema(description = "生效结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "结束时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime endTime;
@Schema(description = "状态(0开启 1关闭)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
private Integer status;
@Schema(description = "备注", example = "某某航司临时账号")
private String remark;
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.member.controller.admin.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 航空账号创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberAirlineAccountCreateReqVO extends MemberAirlineAccountBaseVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.member.controller.admin.airline.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 航空账号分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberAirlineAccountPageReqVO extends PageParam {
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", example = "CZ")
private String channel;
@Schema(description = "状态(0开启 1关闭)", example = "0")
private Integer status;
@Schema(description = "设备唯一识别号", example = "A1B2C3D4")
private String deviceId;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.member.controller.admin.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 航空账号 Response VO")
@Data
public class MemberAirlineAccountRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000")
private String mobile;
@Schema(description = "品牌渠道(CZ南航/MU东航/HU海航)", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
private String channel;
@Schema(description = "生效开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime startTime;
@Schema(description = "生效结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime endTime;
@Schema(description = "设备唯一识别号", example = "A1B2C3D4")
private String deviceId;
@Schema(description = "状态(0开启 1关闭)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status;
@Schema(description = "备注")
private String remark;
@Schema(description = "关联会员用户编号")
private Long userId;
@Schema(description = "是否已过期")
private Boolean expired;
@Schema(description = "是否尚未生效")
private Boolean notStarted;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.member.controller.admin.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 航空账号更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MemberAirlineAccountUpdateReqVO extends MemberAirlineAccountBaseVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "编号不能为空")
private Long id;
@Schema(description = "密码(不传或空则不修改)", example = "123456")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}
@@ -71,6 +71,14 @@ public class MemberUserController {
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "更新会员用户状态(禁用后踢出 App 登录,且无法再登录)")
@PreAuthorize("@ss.hasPermission('member:user:update')")
public CommonResult<Boolean> updateUserStatus(@Valid @RequestBody MemberUserUpdateStatusReqVO reqVO) {
memberUserService.updateUserStatus(reqVO.getId(), reqVO.getStatus());
return success(true);
}
@GetMapping("/fill-missing-user-codes")
@Operation(summary = "为历史会员补齐用户 Code")
// @PreAuthorize("@ss.hasPermission('member:user:update')")
@@ -41,6 +41,12 @@ public class MemberUserPageReqVO extends PageParam {
@Schema(description = "用户分组编号", example = "1")
private Long groupId;
@Schema(description = "用户 Code(App 我的页面展示的 ID)", example = "924199")
private String userCode;
@Schema(description = "性别:1 男 2 女", example = "1")
private Integer sex;
// TODO 芋艿:注册用户类型;
// TODO 芋艿:登录用户类型;
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.member.controller.admin.user.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 会员用户更新状态 Request VO")
@Data
public class MemberUserUpdateStatusReqVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "用户编号不能为空")
private Long id;
@Schema(description = "状态,见 CommonStatusEnum 枚举。0 开启,1 禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "状态不能为空")
@InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}")
private Integer status;
}
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.member.controller.app.airline;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.member.controller.app.airline.vo.AirlineFlightVO;
import cn.iocoder.yudao.module.member.controller.app.airline.vo.AirlinePointVO;
import cn.iocoder.yudao.module.member.service.airline.MemberAirlineDataService;
import io.swagger.v3.oas.annotations.Operation;
import javax.annotation.security.PermitAll;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* 免登录接口(@PermitAll):身份由 mobile + channel + deviceId 三要素定位,
* 与 airline-check 相同的账号状态/有效期/设备绑定校验,防冒用。
*/
@Tag(name = "用户 APP - 航空 App 数据云同步(自建航线/我的积点钱包)")
@RestController
@RequestMapping("/member/airline-data")
@Validated
public class AppAirlineDataController {
@Resource
private MemberAirlineDataService airlineDataService;
// ─── 航线 ────────────────────────────────────────────────
@GetMapping("/flight/list")
@Operation(summary = "航线列表")
@PermitAll
public CommonResult<List<AirlineFlightVO>> flightList(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel) {
return success(airlineDataService.getFlightList(mobile, deviceId, channel));
}
@PostMapping("/flight/create")
@Operation(summary = "新增航线")
@PermitAll
public CommonResult<Long> flightCreate(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel,
@RequestBody AirlineFlightVO vo) {
return success(airlineDataService.createFlight(mobile, deviceId, channel, vo));
}
@PutMapping("/flight/update")
@Operation(summary = "更新航线")
@PermitAll
public CommonResult<Boolean> flightUpdate(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel,
@RequestBody AirlineFlightVO vo) {
airlineDataService.updateFlight(mobile, deviceId, channel, vo);
return success(true);
}
@DeleteMapping("/flight/delete")
@Operation(summary = "删除航线")
@PermitAll
public CommonResult<Boolean> flightDelete(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel,
@RequestParam("id") Long id) {
airlineDataService.deleteFlight(mobile, deviceId, channel, id);
return success(true);
}
// ─── 我的:积点/钱包 ─────────────────────────────────────
@GetMapping("/point/get")
@Operation(summary = "我的积点/钱包")
@PermitAll
public CommonResult<AirlinePointVO> pointGet(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel) {
return success(airlineDataService.getPoint(mobile, deviceId, channel));
}
@PutMapping("/point/update")
@Operation(summary = "保存我的积点/钱包")
@PermitAll
public CommonResult<Boolean> pointUpdate(@RequestParam("mobile") String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam("channel") String channel,
@RequestBody AirlinePointVO vo) {
airlineDataService.updatePoint(mobile, deviceId, channel, vo);
return success(true);
}
}
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.member.controller.app.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 APP - 航空 App 自建航线 VO")
@Data
public class AirlineFlightVO {
@Schema(description = "编号(新增时不传,更新时必传)", example = "1")
private Long id;
@Schema(description = "航班号", example = "CZ3101")
private String flightNo;
@Schema(description = "任务/航班名称")
private String title;
@Schema(description = "出发城市")
private String beginCity;
@Schema(description = "出发机场")
private String beginAirport;
@Schema(description = "到达城市")
private String endCity;
@Schema(description = "到达机场")
private String endAirport;
@Schema(description = "舱位/班次")
private String classes;
@Schema(description = "状态")
private String status;
@Schema(description = "计划起飞时间")
private String flyBeginDate0;
@Schema(description = "计划到达时间")
private String flyEndDate0;
@Schema(description = "实际起飞时间")
private String flyBeginDate1;
@Schema(description = "实际到达时间")
private String flyEndDate1;
@Schema(description = "签到时间")
private String signInTime;
@Schema(description = "机号/编号")
private String aircraftNo;
@Schema(description = "航司")
private String airline;
@Schema(description = "机型")
private String aircraftType;
@Schema(description = "登机口")
private String gate;
@Schema(description = "出发机场停机位")
private String beginStand;
@Schema(description = "到达机场停机位")
private String endStand;
@Schema(description = "货关时间")
private String cargoClose;
@Schema(description = "客关时间")
private String paxClose;
@Schema(description = "离港时间")
private String leavePort;
@Schema(description = "起飞时间")
private String takeOff;
@Schema(description = "落地时间")
private String land;
@Schema(description = "到港时间")
private String arrivePort;
@Schema(description = "前序航班")
private String prevFlight;
}
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.member.controller.app.airline.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 APP - 航空 App 我的积点/钱包 VO")
@Data
public class AirlinePointVO {
@Schema(description = "当月飞行", example = "12")
private String currentMonth;
@Schema(description = "年度飞行", example = "130")
private String currentYear;
@Schema(description = "绩效积点", example = "08")
private String jixiao;
@Schema(description = "延误积点", example = "00")
private String yanwu;
@Schema(description = "当前积点", example = "260")
private String currentPoint;
@Schema(description = "钱包余额", example = "1280.00")
private String wallet;
}
@@ -49,6 +49,27 @@ public class AppAuthController {
return success(authService.login(reqVO));
}
@PostMapping("/airline-login")
@Operation(summary = "航空账号登录(手机 + 密码 + 设备唯一识别号)")
@PermitAll
public CommonResult<AppAuthLoginRespVO> airlineLogin(@RequestBody @Valid AppAuthAirlineLoginReqVO reqVO) {
return success(authService.airlineLogin(reqVO));
}
@GetMapping("/airline-check")
@Operation(summary = "航空账号会话校验(免登录,兼容新旧客户端:Token/手机号+渠道/设备识别号 任一定位)")
@Parameter(name = "deviceId", description = "设备唯一识别号", required = true)
@Parameter(name = "mobile", description = "手机号(新客户端传)")
@Parameter(name = "channel", description = "品牌渠道 CZ/MU/HU(新客户端传)")
@PermitAll
public CommonResult<Boolean> airlineCheck(
@RequestParam(value = "mobile", required = false) String mobile,
@RequestParam("deviceId") String deviceId,
@RequestParam(value = "channel", required = false) String channel) {
authService.airlineCheck(getLoginUserId(), mobile, deviceId, channel);
return success(true);
}
@PostMapping("/logout")
@Operation(summary = "登出系统")
@PermitAll
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import cn.iocoder.yudao.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Schema(description = "用户 APP - 航空账号登录 Request VO")
@Data
public class AppAuthAirlineLoginReqVO {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000")
@NotEmpty(message = "手机号不能为空")
@Mobile
private String mobile;
@Schema(description = "品牌渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "CZ")
@NotEmpty(message = "品牌渠道不能为空")
@Pattern(regexp = "CZ|MU|HU", message = "品牌渠道仅支持 CZ(南航)/ MU(东航)/ HU(海航)")
private String channel;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@Schema(description = "手机唯一识别号(重装/重启后须保持不变)", requiredMode = Schema.RequiredMode.REQUIRED,
example = "android-id-or-keychain-uuid")
@NotEmpty(message = "设备唯一识别号不能为空")
@Length(min = 8, max = 128, message = "设备唯一识别号长度为 8-128 位")
private String deviceId;
}
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.member.client.vo.chat.friend.ChatAddFriendReqVO;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatPrivateMessageSendReqVO;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatPrivateMessageSendResVO;
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeService;
import cn.iocoder.yudao.module.member.service.push.MemberPushService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
@@ -27,6 +28,8 @@ public class AppChatController {
@Resource
private ChatApi chatApi;
@Resource
private MemberPushService memberPushService;
@PostMapping("/send-private-msg")
@Operation(summary = "发送私聊消息(有会员限制)")
@@ -35,7 +38,11 @@ public class AppChatController {
openTypeService.isUserCanSendMessage(sendId, body.getRecvId());
body.setSendId(sendId);
body.setTerminal(1);
return success(chatApi.send(body));
ChatPrivateMessageSendResVO result = chatApi.send(body);
if (result != null) {
memberPushService.pushPrivateMessage(body);
}
return success(result);
}
@GetMapping("/add-friend")
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.member.controller.app.comauth.vo.CreateManAuthReq
import cn.iocoder.yudao.module.member.controller.app.comauth.vo.CreateWechatAuthReqVo;
import cn.iocoder.yudao.module.member.controller.app.comauth.vo.CreateWechatErrorAuthReqVo;
import cn.iocoder.yudao.module.member.controller.app.comauth.vo.CreateWomanAuthReqVo;
import cn.iocoder.yudao.module.member.client.vo.thirdfaceverify.ThirdFindFaceVerifyResVo;
import cn.iocoder.yudao.module.member.dal.dataobject.comauth.ComAuthDO;
import cn.iocoder.yudao.module.member.service.comauth.ComAuthService;
import io.swagger.v3.oas.annotations.Operation;
@@ -47,12 +48,6 @@ public class AppComAuthController {
return success(comAuthService.createWechatAuth(dto));
}
@PostMapping("/create-man-auth")
@Operation(summary = "创建男性认证")
public CommonResult<ComAuthDO> createManAuth(@Valid @RequestBody CreateManAuthReqVo dto) {
dto.setMemberId(getLoginUserId());
return success(comAuthService.createManAuth(dto));
}
@PostMapping("/create-woman-auth")
@Operation(summary = "创建女性认证")
@@ -81,4 +76,10 @@ public class AppComAuthController {
return success(true);
}
@GetMapping("/find-face-verify")
@Operation(summary = "查询阿里云人脸认证结果(SDK 失败排查)")
public CommonResult<ThirdFindFaceVerifyResVo> findFaceVerify(@RequestParam("authId") Long authId) {
return success(comAuthService.findFaceVerify(authId, getLoginUserId()));
}
}
@@ -19,8 +19,7 @@ public class CreateWechatAuthReqVo {
@NotEmpty(message = "微信号不能为空")
private String wechatNumber;
@Schema(description = "微信二维码")
@NotEmpty(message = "微信二维码不能为空")
@Schema(description = "微信二维码(选填)")
private String wechatQrcode;
}
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.member.controller.app.interaction;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionRespVO;
import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionSummaryRespVO;
import cn.iocoder.yudao.module.member.service.interaction.MemberInteractionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 App - 互动消息")
@RestController
@RequestMapping("/member/interaction")
@Validated
public class AppMemberInteractionController {
@Resource
private MemberInteractionService interactionService;
@GetMapping("/summary")
@Operation(summary = "互动消息摘要(未读数 + 最近一条)")
public CommonResult<AppInteractionSummaryRespVO> getSummary() {
return success(interactionService.getSummary(getLoginUserId()));
}
@GetMapping("/page")
@Operation(summary = "互动消息分页")
public CommonResult<PageResult<AppInteractionRespVO>> getPage(@Valid PageParam pageParam) {
return success(interactionService.getPage(getLoginUserId(), pageParam));
}
@PostMapping("/read-all")
@Operation(summary = "全部标为已读")
public CommonResult<Boolean> readAll() {
interactionService.readAll(getLoginUserId());
return success(true);
}
}
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.member.controller.app.interaction.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "用户 App - 互动消息 Response VO")
@Data
public class AppInteractionRespVO {
@Schema(description = "编号")
private Long id;
@Schema(description = "类型:VIEWED / WECHAT_UNLOCKED")
private String bizType;
@Schema(description = "文案,如「张三 查看了你」")
private String content;
@Schema(description = "操作人编号")
private Long actorId;
@Schema(description = "操作人头像")
private String actorAvatar;
@Schema(description = "操作人昵称")
private String actorNickname;
@Schema(description = "是否已读")
private Boolean readStatus;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.member.controller.app.interaction.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "用户 App - 互动消息摘要 Response VO")
@Data
public class AppInteractionSummaryRespVO {
@Schema(description = "未读数")
private Long unreadCount;
@Schema(description = "最近一条文案")
private String lastContent;
@Schema(description = "最近一条时间")
private LocalDateTime lastTime;
}
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.member.controller.app.push;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.member.controller.app.push.vo.AppPushBindReqVO;
import cn.iocoder.yudao.module.member.controller.app.push.vo.AppPushUnbindReqVO;
import cn.iocoder.yudao.module.member.service.push.MemberPushDeviceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "用户 App - 推送设备")
@RestController
@RequestMapping("/member/push")
@Validated
public class AppMemberPushController {
@Resource
private MemberPushDeviceService pushDeviceService;
@PostMapping("/bind")
@Operation(summary = "绑定推送设备(极光 RegistrationID)")
public CommonResult<Boolean> bind(@Valid @RequestBody AppPushBindReqVO reqVO) {
pushDeviceService.bind(getLoginUserId(), reqVO);
return success(true);
}
@PostMapping("/unbind")
@Operation(summary = "解绑推送设备")
public CommonResult<Boolean> unbind(@Valid @RequestBody AppPushUnbindReqVO reqVO) {
pushDeviceService.unbind(getLoginUserId(), reqVO.getRegistrationId());
return success(true);
}
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.member.controller.app.push.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Schema(description = "用户 App - 推送设备绑定 Request VO")
@Data
public class AppPushBindReqVO {
@Schema(description = "极光 RegistrationID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1a2b3c")
@NotBlank(message = "registrationId 不能为空")
private String registrationId;
@Schema(description = "极光别名", example = "u1024")
private String alias;
@Schema(description = "平台:ios / android", requiredMode = Schema.RequiredMode.REQUIRED, example = "android")
@NotBlank(message = "platform 不能为空")
private String platform;
@Schema(description = "推送服务商", example = "jpush")
private String provider;
}
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.member.controller.app.push.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Schema(description = "用户 App - 推送设备解绑 Request VO")
@Data
public class AppPushUnbindReqVO {
@Schema(description = "极光 RegistrationID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1a2b3c")
@NotBlank(message = "registrationId 不能为空")
private String registrationId;
}
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanRespVO;
import cn.iocoder.yudao.module.member.service.rideplan.MemberRidePlanService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.annotation.security.PermitAll;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 骑遇发车局:
* - 查询(page/get)@PermitAll 免登录,游客可浏览, RespVO 已做字段脱敏(只暴露昵称/头像)
* - 发布/加入/取消加入/举报 需登录(/app-api/** 默认拦截,未带有效 token 返回 401)
*/
@Tag(name = "用户 APP - 骑遇发车局")
@RestController
@RequestMapping("/member/ride-plan")
@Validated
public class AppRidePlanController {
@Resource
private MemberRidePlanService ridePlanService;
@GetMapping("/page")
@Operation(summary = "发车局分页(免登录)", description = "游客可查;登录后返回 joinedByMe")
@PermitAll
public CommonResult<PageResult<AppRidePlanRespVO>> page(@Valid AppRidePlanPageReqVO reqVO) {
return success(ridePlanService.getRidePlanPage(reqVO, getLoginUserId()));
}
@GetMapping("/get")
@Operation(summary = "发车局详情(免登录)")
@Parameter(name = "id", description = "发车局编号", required = true, example = "1")
@PermitAll
public CommonResult<AppRidePlanRespVO> get(@RequestParam("id") Long id) {
return success(ridePlanService.getRidePlan(id, getLoginUserId()));
}
@PostMapping("/create")
@Operation(summary = "发布发车局(需登录)")
public CommonResult<Long> create(@Valid @RequestBody AppRidePlanCreateReqVO reqVO) {
return success(ridePlanService.createRidePlan(getLoginUserId(), reqVO));
}
@PostMapping("/join")
@Operation(summary = "加入发车局(需登录)")
@Parameter(name = "planId", description = "发车局编号", required = true, example = "1")
public CommonResult<Boolean> join(@RequestParam("planId") Long planId) {
ridePlanService.joinRidePlan(getLoginUserId(), planId);
return success(true);
}
@PostMapping("/cancel-join")
@Operation(summary = "取消加入发车局(需登录)")
@Parameter(name = "planId", description = "发车局编号", required = true, example = "1")
public CommonResult<Boolean> cancelJoin(@RequestParam("planId") Long planId) {
ridePlanService.cancelJoinRidePlan(getLoginUserId(), planId);
return success(true);
}
@PostMapping("/report")
@Operation(summary = "举报发车局(需登录),达到阈值自动下架")
@Parameter(name = "planId", description = "发车局编号", required = true, example = "1")
public CommonResult<Boolean> report(@RequestParam("planId") Long planId) {
ridePlanService.reportRidePlan(getLoginUserId(), planId);
return success(true);
}
}
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordCreateReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordRespVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordStatsRespVO;
import cn.iocoder.yudao.module.member.service.rideplan.MemberRideRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 骑遇骑行记录(战报):
* - 战报墙 page / 详情 get / 天梯榜 rank @PermitAll 免登录,游客可浏览,RespVO 已做字段脱敏(只暴露昵称/头像)
* - 上报 create / 点赞 like / 举报 report 需登录(/app-api/** 默认拦截,未带有效 token 返回 401)
*/
@Tag(name = "用户 APP - 骑遇骑行记录")
@RestController
@RequestMapping("/member/ride-record")
@Validated
public class AppRideRecordController {
@Resource
private MemberRideRecordService rideRecordService;
@PostMapping("/create")
@Operation(summary = "上报骑行战报(需登录)", description = "完骑后提交;cornerName 有值则参与天梯榜")
public CommonResult<Long> create(@Valid @RequestBody AppRideRecordCreateReqVO reqVO) {
return success(rideRecordService.createRideRecord(getLoginUserId(), reqVO));
}
@GetMapping("/page")
@Operation(summary = "战报墙分页(免登录)", description = "游客可查;登录后返回 likedByMe")
@PermitAll
public CommonResult<PageResult<AppRideRecordRespVO>> page(@Valid AppRideRecordPageReqVO reqVO) {
return success(rideRecordService.getRideRecordPage(reqVO, getLoginUserId()));
}
@GetMapping("/my")
@Operation(summary = "我的骑行记录分页(需登录)")
public CommonResult<PageResult<AppRideRecordRespVO>> my(@Valid AppRideRecordPageReqVO reqVO) {
return success(rideRecordService.getMyRideRecordPage(getLoginUserId(), reqVO));
}
@GetMapping("/get")
@Operation(summary = "战报详情(免登录,含轨迹)")
@Parameter(name = "id", description = "战报编号", required = true, example = "1")
@PermitAll
public CommonResult<AppRideRecordRespVO> get(@RequestParam("id") Long id) {
return success(rideRecordService.getRideRecord(id, getLoginUserId()));
}
@GetMapping("/rank")
@Operation(summary = "弯道天梯榜(免登录)", description = "按路线+弯道分组,组内按压弯角度降序;支持过滤")
@PermitAll
public CommonResult<List<AppRideRecordRespVO.CornerBoard>> rank(
@RequestParam(value = "roadName", required = false)
@Size(max = 50, message = "路线名长度不能超过 50") String roadName,
@RequestParam(value = "cornerName", required = false)
@Size(max = 50, message = "弯道名长度不能超过 50") String cornerName) {
return success(rideRecordService.getCornerRank(roadName, cornerName));
}
@GetMapping("/stats")
@Operation(summary = "批量骑手战报统计(免登录)", description = "返回每个骑手的战报数/最佳压弯/总里程;最多 100 人")
@PermitAll
public CommonResult<List<AppRideRecordStatsRespVO>> stats(
@RequestParam("userIds") @Size(max = 100, message = "一次最多查询 100 个骑手") List<Long> userIds) {
return success(rideRecordService.getRideRecordStats(userIds));
}
@PostMapping("/like")
@Operation(summary = "点赞/取消点赞(需登录)", description = "幂等切换,返回操作后是否处于点赞状态")
@Parameter(name = "recordId", description = "战报编号", required = true, example = "1")
public CommonResult<Boolean> like(@RequestParam("recordId") Long recordId) {
return success(rideRecordService.toggleLike(getLoginUserId(), recordId));
}
@PostMapping("/report")
@Operation(summary = "举报战报(需登录)", description = "达到阈值自动下架")
@Parameter(name = "recordId", description = "战报编号", required = true, example = "1")
public CommonResult<Boolean> report(@RequestParam("recordId") Long recordId) {
rideRecordService.reportRideRecord(getLoginUserId(), recordId);
return success(true);
}
}
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideTagRespVO;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideUserTagUpdateReqVO;
import cn.iocoder.yudao.module.member.service.rideplan.MemberRidePlanService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.annotation.security.PermitAll;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
* 骑遇骑行标签:
* - 标签字典 list @PermitAll 免登录(发局选标签、列表展示用)
* - 我的标签读写 需登录
*/
@Tag(name = "用户 APP - 骑遇骑行标签")
@RestController
@RequestMapping("/member/ride-tag")
@Validated
public class AppRideTagController {
@Resource
private MemberRidePlanService ridePlanService;
@GetMapping("/list")
@Operation(summary = "骑行标签字典(免登录,只返回启用的)")
@PermitAll
public CommonResult<List<AppRideTagRespVO>> list() {
return success(ridePlanService.getRideTagList());
}
@GetMapping("/my")
@Operation(summary = "我的骑行标签(需登录)")
public CommonResult<List<AppRideTagRespVO>> my() {
return success(ridePlanService.getMyRideTags(getLoginUserId()));
}
@PutMapping("/my")
@Operation(summary = "全量替换我的骑行标签(需登录)")
public CommonResult<Boolean> updateMy(@Valid @RequestBody AppRideUserTagUpdateReqVO reqVO) {
ridePlanService.updateMyRideTags(getLoginUserId(), reqVO);
return success(true);
}
}
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 APP - 发布发车局 Request VO")
@Data
public class AppRidePlanCreateReqVO {
@Schema(description = "路线名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "南昆山经典环线")
@NotBlank(message = "路线名称不能为空")
@Size(max = 100, message = "路线名称长度不能超过 100")
private String routeName;
@Schema(description = "集合地点", requiredMode = Schema.RequiredMode.REQUIRED, example = "增城广场加油站")
@NotBlank(message = "集合地点不能为空")
@Size(max = 200, message = "集合地点长度不能超过 200")
private String meetPlace;
@Schema(description = "集合时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "集合时间不能为空")
private LocalDateTime meetTime;
@Schema(description = "全程约多少公里", example = "50")
@DecimalMin(value = "0.0", message = "公里数不能为负数")
private BigDecimal distanceKm;
@Schema(description = "城市(同城过滤)", example = "广州")
@Size(max = 64, message = "城市长度不能超过 64")
private String city;
@Schema(description = "标签名列表", example = "[\"跑山\",\"新手友好\"]")
@Size(max = 5, message = "最多选择 5 个标签")
private List<String> tags;
@Schema(description = "人数上限", example = "10")
@NotNull(message = "人数上限不能为空")
@Min(value = 2, message = "人数上限最少 2 人")
@Max(value = 100, message = "人数上限最多 100 人")
private Integer capacity;
}
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Size;
@Schema(description = "用户 APP - 发车局分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AppRidePlanPageReqVO extends PageParam {
@Schema(description = "城市(同城过滤)", example = "广州")
@Size(max = 64, message = "城市长度不能超过 64")
private String city;
@Schema(description = "关键词,匹配路线名称", example = "南昆山")
@Size(max = 100, message = "关键词长度不能超过 100")
private String keyword;
@Schema(description = "标签名,精确匹配", example = "跑山")
@Size(max = 32, message = "标签长度不能超过 32")
private String tag;
}
@@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 APP - 发车局 Response VO(免登录可查,已做字段脱敏)")
@Data
public class AppRidePlanRespVO {
@Schema(description = "发车局编号", example = "1")
private Long id;
@Schema(description = "路线名称", example = "南昆山经典环线")
private String routeName;
@Schema(description = "集合地点", example = "增城广场加油站")
private String meetPlace;
@Schema(description = "集合时间")
private LocalDateTime meetTime;
@Schema(description = "全程约多少公里", example = "50")
private BigDecimal distanceKm;
@Schema(description = "城市", example = "广州")
private String city;
@Schema(description = "标签名列表", example = "[\"跑山\",\"新手友好\"]")
private List<String> tags;
@Schema(description = "人数上限", example = "10")
private Integer capacity;
@Schema(description = "已报名人数", example = "3")
private Integer joinCount;
// —— 发起人(只暴露昵称/头像,不含手机号等敏感字段)——
@Schema(description = "发起人编号", example = "1024")
private Long hostUserId;
@Schema(description = "发起人昵称", example = "山路信天游")
private String hostNickname;
@Schema(description = "发起人头像")
private String hostAvatar;
@Schema(description = "已报名骑手列表(昵称 + 头像)")
private List<JoinUser> joinedUsers;
@Schema(description = "当前登录用户是否已加入(游客恒为 false)")
private Boolean joinedByMe;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "已报名骑手(只暴露昵称 + 头像)")
@Data
public static class JoinUser {
@Schema(description = "用户编号", example = "2048")
private Long userId;
@Schema(description = "昵称", example = "压弯不压车")
private String nickname;
@Schema(description = "头像")
private String avatar;
}
}
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
@Schema(description = "用户 APP - 骑行战报上报 Request VO(完骑后提交,需登录)")
@Data
public class AppRideRecordCreateReqVO {
@Schema(description = "战报标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "南昆山 63 连弯,今天左弯终于不虚了")
@NotBlank(message = "战报标题不能为空")
@Size(max = 100, message = "战报标题不能超过 100 字")
private String title;
@Schema(description = "路线名(可选)", example = "南昆山")
@Size(max = 50, message = "路线名不能超过 50 字")
private String roadName;
@Schema(description = "弯道名(可选,有值参与天梯榜)", example = "观音潭右弯")
@Size(max = 50, message = "弯道名不能超过 50 字")
private String cornerName;
@Schema(description = "里程(公里)", requiredMode = Schema.RequiredMode.REQUIRED, example = "87.3")
@NotNull(message = "里程不能为空")
@DecimalMin(value = "0.1", message = "里程至少 0.1 公里")
@DecimalMax(value = "5000", message = "里程异常")
private BigDecimal distanceKm;
@Schema(description = "骑行时长(秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5760")
@NotNull(message = "骑行时长不能为空")
@Min(value = 30, message = "骑行时长太短")
private Integer durationSec;
@Schema(description = "极速(km/h)", requiredMode = Schema.RequiredMode.REQUIRED, example = "138")
@NotNull(message = "极速不能为空")
@DecimalMin(value = "0", message = "极速不能为负")
@DecimalMax(value = "500", message = "极速异常")
private BigDecimal topSpeedKmh;
@Schema(description = "过弯次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "63")
@NotNull(message = "过弯次数不能为空")
@Min(value = 0, message = "过弯次数不能为负")
@Max(value = 10000, message = "过弯次数异常")
private Integer curveCount;
@Schema(description = "最大压弯角度(度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "47.2")
@NotNull(message = "最大压弯角度不能为空")
@DecimalMin(value = "0", message = "压弯角度不能为负")
@DecimalMax(value = "90", message = "压弯角度异常")
private BigDecimal maxLean;
@Schema(description = "轨迹点 JSON:[{\"lat\":23.1,\"lng\":113.3,\"speed\":88}],可选,仅详情接口回显")
@Size(max = 2_000_000, message = "轨迹数据过大")
private String trackJson;
}
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Size;
@Schema(description = "用户 APP - 骑行战报分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AppRideRecordPageReqVO extends PageParam {
@Schema(description = "按骑手过滤(查某人战报)", example = "1024")
private Long userId;
@Schema(description = "路线名过滤", example = "南昆山")
@Size(max = 50, message = "路线名长度不能超过 50")
private String roadName;
}
@@ -0,0 +1,115 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 APP - 骑行战报 Response VO(列表不含轨迹,详情含)")
@Data
public class AppRideRecordRespVO {
@Schema(description = "战报编号", example = "1")
private Long id;
@Schema(description = "战报标题", example = "南昆山 63 连弯,今天左弯终于不虚了")
private String title;
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "里程(公里)", example = "87.3")
private BigDecimal distanceKm;
@Schema(description = "骑行时长(秒)", example = "5760")
private Integer durationSec;
@Schema(description = "极速(km/h)", example = "138")
private BigDecimal topSpeedKmh;
@Schema(description = "过弯次数", example = "63")
private Integer curveCount;
@Schema(description = "最大压弯角度(度)", example = "47.2")
private BigDecimal maxLean;
@Schema(description = "轨迹点 JSON,仅详情接口返回")
private String trackJson;
@Schema(description = "点赞数", example = "128")
private Integer likeCount;
@Schema(description = "当前登录用户是否已点赞(游客恒为 false)")
private Boolean likedByMe;
// —— 骑手(只暴露昵称/头像,不含手机号等敏感字段)——
@Schema(description = "骑手编号", example = "1024")
private Long userId;
@Schema(description = "骑手昵称", example = "山路信天游")
private String nickname;
@Schema(description = "骑手头像")
private String avatar;
@Schema(description = "发布时间")
private LocalDateTime createTime;
@Schema(description = "天梯榜条目(rank 接口专用)")
@Data
public static class RankItem {
@Schema(description = "名次,从 1 开始", example = "1")
private Integer rank;
@Schema(description = "战报编号", example = "5")
private Long recordId;
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "最大压弯角度(度)", example = "52.0")
private BigDecimal maxLean;
@Schema(description = "极速(km/h)", example = "62")
private BigDecimal topSpeedKmh;
@Schema(description = "骑手编号", example = "1024")
private Long userId;
@Schema(description = "骑手昵称", example = "压弯不压车")
private String nickname;
@Schema(description = "骑手头像")
private String avatar;
@Schema(description = "骑行时间")
private LocalDateTime createTime;
}
@Schema(description = "弯道榜单(rank 接口返回,按弯道分组)")
@Data
public static class CornerBoard {
@Schema(description = "路线名", example = "南昆山")
private String roadName;
@Schema(description = "弯道名", example = "观音潭右弯")
private String cornerName;
@Schema(description = "该弯道上榜条目(已按压弯角度降序)")
private List<RankItem> entries;
}
}
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 用户 APP - 骑手战报统计 Response VO
*
* 供首页骑友列表 / 骑友主页展示「战报数 / 最佳压弯 / 总里程」骑行数据行,
* 批量接口避免列表逐人查询。
*/
@Schema(description = "用户 APP - 骑手战报统计 Response VO")
@Data
public class AppRideRecordStatsRespVO {
@Schema(description = "骑手用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "战报数量(正常状态)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
private Long reportCount;
@Schema(description = "最佳压弯角度(度)", requiredMode = Schema.RequiredMode.REQUIRED, example = "38.5")
private Double bestLean;
@Schema(description = "总里程(公里)", requiredMode = Schema.RequiredMode.REQUIRED, example = "260.8")
private Double totalKm;
}
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 APP - 骑行标签 Response VO")
@Data
public class AppRideTagRespVO {
@Schema(description = "标签编号", example = "1")
private Long id;
@Schema(description = "标签名", example = "跑山党")
private String name;
}
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.member.controller.app.rideplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Schema(description = "用户 APP - 设置我的骑行标签 Request VO")
@Data
public class AppRideUserTagUpdateReqVO {
@Schema(description = "标签编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2]")
@NotEmpty(message = "标签编号列表不能为空")
private List<Long> tagIds;
}
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.member.controller.app.user;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.member.controller.app.comauth.vo.CreateWechatAuthReqVo;
import cn.iocoder.yudao.module.member.service.comauth.ComAuthService;
@@ -16,6 +17,8 @@ import cn.iocoder.yudao.module.member.service.level.MemberLevelService;
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeService;
import cn.iocoder.yudao.module.member.service.user.MemberRecentTagService;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
@@ -28,8 +31,10 @@ import javax.validation.Valid;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.AUTH_LOGIN_USER_DISABLED;
@Tag(name = "用户 APP - 用户个人中心")
@RestController
@@ -50,11 +55,24 @@ public class AppMemberUserController {
private ComAuthService comAuthService;
@Resource
private LocationService locationService;
@Resource
private OAuth2TokenApi oauth2TokenApi;
@GetMapping("/get")
@Operation(summary = "获得基本信息")
public CommonResult<AppMemberUserInfoRespVO> getUserInfo() {
MemberUserDO user = userService.getUser(getLoginUserId());
// 禁用态已由 MemberUserDisableInterceptor 统一拦截;此处再兜底一次
if (user == null || CommonStatusEnum.isDisable(user.getStatus())) {
if (user != null) {
try {
oauth2TokenApi.removeAccessTokenByUserId(user.getId(), UserTypeEnum.MEMBER.getValue());
} catch (Exception e) {
log.warn("禁用用户拉资料时清 token 失败, userId={}", user.getId(), e);
}
}
throw exception(AUTH_LOGIN_USER_DISABLED);
}
MemberLevelDO level = levelService.getLevel(user.getLevelId());
AppMemberUserInfoRespVO result = MemberUserConvert.INSTANCE.convert(user, level);
// 获取用户tag
@@ -82,11 +82,11 @@ public class AppViewController {
put("search-action", "SQUARE_VIP_MALE");
put("label", "VIP男士");
}});
menu.add(new HashMap<String, String>() {{
put("code", "5000");
put("search-action", "SQUARE_FEMALE");
put("label", "女生");
}});
// menu.add(new HashMap<String, String>() {{
// put("code", "5000");
// put("search-action", "SQUARE_FEMALE");
// put("label", "女生");
// }});
}
return success(menu);
}
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.member.dal.dataobject.airline;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 航空账号 DO
*/
@TableName("member_airline_account")
@KeySequence("member_airline_account_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberAirlineAccountDO extends TenantBaseDO {
@TableId
private Long id;
/** 手机号(同一手机号可在不同渠道各有一个账号) */
private String mobile;
/** 品牌渠道:CZ 南航 / MU 东航 / HU 海航 */
private String channel;
/** 密码(BCrypt) */
private String password;
/** 生效开始时间 */
private LocalDateTime startTime;
/** 生效结束时间 */
private LocalDateTime endTime;
/**
* 手机唯一识别号。首次登录绑定;客户端需保证重装/重启后不变(如 Android ANDROID_ID、iOS Keychain UUID)。
*/
private String deviceId;
/**
* 状态:0 开启 1 关闭,见 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
*/
private Integer status;
/** 备注 */
private String remark;
/** 关联会员用户编号 */
private Long userId;
}
@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.member.dal.dataobject.airline;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 航空 App 用户自建航线 DO
*/
@TableName("member_airline_flight")
@KeySequence("member_airline_flight_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberAirlineFlightDO extends TenantBaseDO {
@TableId
private Long id;
/** 航空账号编号 member_airline_account.id */
private Long accountId;
/** 航班号 */
private String flightNo;
/** 任务/航班名称 */
private String title;
/** 出发城市 */
private String beginCity;
/** 出发机场 */
private String beginAirport;
/** 到达城市 */
private String endCity;
/** 到达机场 */
private String endAirport;
/** 舱位/班次 */
private String classes;
/** 状态 */
private String status;
/** 计划起飞时间 */
private String flyBeginDate0;
/** 计划到达时间 */
private String flyEndDate0;
/** 实际起飞时间 */
private String flyBeginDate1;
/** 实际到达时间 */
private String flyEndDate1;
/** 签到时间 */
private String signInTime;
/** 机号/编号 */
private String aircraftNo;
/** 航司 */
private String airline;
/** 机型 */
private String aircraftType;
/** 登机口 */
private String gate;
/** 出发机场停机位 */
private String beginStand;
/** 到达机场停机位 */
private String endStand;
/** 货关时间 */
private String cargoClose;
/** 客关时间 */
private String paxClose;
/** 离港时间 */
private String leavePort;
/** 起飞时间 */
private String takeOff;
/** 落地时间 */
private String land;
/** 到港时间 */
private String arrivePort;
/** 前序航班 */
private String prevFlight;
}
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.member.dal.dataobject.airline;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 航空 App 我的积点/钱包 DO(每账号一行)
*/
@TableName("member_airline_point")
@KeySequence("member_airline_point_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberAirlinePointDO extends TenantBaseDO {
@TableId
private Long id;
/** 航空账号编号 member_airline_account.id */
private Long accountId;
/** 当月飞行 */
private String currentMonth;
/** 年度飞行 */
private String currentYear;
/** 绩效积点 */
private String jixiao;
/** 延误积点 */
private String yanwu;
/** 当前积点 */
private String currentPoint;
/** 钱包余额 */
private String wallet;
}
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.member.dal.dataobject.interaction;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 会员互动消息 DO
*/
@TableName("member_interaction")
@KeySequence("member_interaction_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberInteractionDO extends TenantBaseDO {
@TableId
private Long id;
/**
* 接收人(被查看 / 被解锁)
*/
private Long receiverId;
/**
* 操作人
*/
private Long actorId;
/**
* VIEWED / WECHAT_UNLOCKED
*/
private String bizType;
/**
* 关联业务 id
*/
private Long refId;
/**
* 是否已读
*/
private Boolean readStatus;
}
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.member.dal.dataobject.push;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 会员推送设备 DO
*/
@TableName("member_push_device")
@KeySequence("member_push_device_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberPushDeviceDO extends TenantBaseDO {
@TableId
private Long id;
/**
* 会员编号
*/
private Long memberId;
/**
* 极光 RegistrationID
*/
private String registrationId;
/**
* 极光别名
*/
private String alias;
/**
* 平台:ios / android
*/
private String platform;
/**
* 推送服务商:jpush
*/
private String provider;
}
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 骑遇发车局 DO
*/
@TableName("member_ride_plan")
@KeySequence("member_ride_plan_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRidePlanDO extends TenantBaseDO {
@TableId
private Long id;
/** 发起人编号 member_user.id */
private Long userId;
/** 路线名称 */
private String routeName;
/** 集合地点 */
private String meetPlace;
/** 集合时间 */
private LocalDateTime meetTime;
/** 全程约多少公里 */
private BigDecimal distanceKm;
/** 城市(同城过滤) */
private String city;
/** 标签名,逗号分隔 */
private String tags;
/** 人数上限 */
private Integer capacity;
/** 已报名人数(冗余计数) */
private Integer joinCount;
/** 被举报次数 */
private Integer reportCount;
/**
* 状态:0 正常 1 已取消/下架
*/
private Integer status;
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 骑遇发车局报名 DO
*/
@TableName("member_ride_plan_join")
@KeySequence("member_ride_plan_join_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRidePlanJoinDO extends TenantBaseDO {
@TableId
private Long id;
/** 发车局编号 member_ride_plan.id */
private Long planId;
/** 报名人编号 member_user.id */
private Long userId;
}
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
* 骑遇骑行记录(战报)DO
*/
@TableName("member_ride_record")
@KeySequence("member_ride_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideRecordDO extends TenantBaseDO {
@TableId
private Long id;
/** 骑手编号 member_user.id */
private Long userId;
/** 战报标题 */
private String title;
/** 路线名(如:南昆山) */
private String roadName;
/** 弯道名(如:观音潭右弯),为空表示非跑山记录 */
private String cornerName;
/** 里程(公里) */
private BigDecimal distanceKm;
/** 骑行时长(秒) */
private Integer durationSec;
/** 极速(km/h) */
private BigDecimal topSpeedKmh;
/** 过弯次数 */
private Integer curveCount;
/** 最大压弯角度(度) */
private BigDecimal maxLean;
/** 轨迹点 JSON([{"lat":..,"lng":..,"speed":..}]),仅详情接口返回 */
private String trackJson;
/** 点赞数(冗余计数) */
private Integer likeCount;
/** 被举报次数 */
private Integer reportCount;
/**
* 状态:0 正常 1 已下架
*/
private Integer status;
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 骑行战报点赞 DO(一条记录一个用户只能赞一次,重复点赞视为取消)
*/
@TableName("member_ride_record_like")
@KeySequence("member_ride_record_like_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideRecordLikeDO extends TenantBaseDO {
@TableId
private Long id;
/** 战报编号 member_ride_record.id */
private Long recordId;
/** 点赞人编号 member_user.id */
private Long userId;
}
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 骑遇骑行标签字典 DO(跑山党 / 新手友好 / 摩旅组队 ...)
*/
@TableName("member_ride_tag")
@KeySequence("member_ride_tag_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideTagDO extends TenantBaseDO {
@TableId
private Long id;
/** 标签名 */
private String name;
/** 显示顺序(越小越靠前) */
private Integer sort;
/**
* 状态:0 启用 1 停用,见 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
*/
private Integer status;
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.member.dal.dataobject.rideplan;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 骑手个人骑行标签关联 DO(Rider.tags)
*/
@TableName("member_ride_user_tag")
@KeySequence("member_ride_user_tag_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberRideUserTagDO extends TenantBaseDO {
@TableId
private Long id;
/** 用户编号 member_user.id */
private Long userId;
/** 标签编号 member_ride_tag.id */
private Long tagId;
}
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.member.dal.mysql.airline;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.controller.admin.airline.vo.MemberAirlineAccountPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineAccountDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberAirlineAccountMapper extends BaseMapperX<MemberAirlineAccountDO> {
default PageResult<MemberAirlineAccountDO> selectPage(MemberAirlineAccountPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.likeIfPresent(MemberAirlineAccountDO::getMobile, reqVO.getMobile())
.eqIfPresent(MemberAirlineAccountDO::getChannel, reqVO.getChannel())
.eqIfPresent(MemberAirlineAccountDO::getStatus, reqVO.getStatus())
.likeIfPresent(MemberAirlineAccountDO::getDeviceId, reqVO.getDeviceId())
.betweenIfPresent(MemberAirlineAccountDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(MemberAirlineAccountDO::getId));
}
default MemberAirlineAccountDO selectByMobileAndChannel(String mobile, String channel) {
return selectOne(new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.eq(MemberAirlineAccountDO::getMobile, mobile)
.eq(MemberAirlineAccountDO::getChannel, channel));
}
default MemberAirlineAccountDO selectByDeviceIdAndChannel(String deviceId, String channel) {
return selectOne(new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.eq(MemberAirlineAccountDO::getDeviceId, deviceId)
.eq(MemberAirlineAccountDO::getChannel, channel));
}
default List<MemberAirlineAccountDO> selectListByDeviceId(String deviceId) {
return selectList(new LambdaQueryWrapperX<MemberAirlineAccountDO>()
.eq(MemberAirlineAccountDO::getDeviceId, deviceId));
}
}
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.member.dal.mysql.airline;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlineFlightDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberAirlineFlightMapper extends BaseMapperX<MemberAirlineFlightDO> {
default List<MemberAirlineFlightDO> selectListByAccountId(Long accountId) {
return selectList(new LambdaQueryWrapperX<MemberAirlineFlightDO>()
.eq(MemberAirlineFlightDO::getAccountId, accountId)
.orderByDesc(MemberAirlineFlightDO::getId));
}
}
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.member.dal.mysql.airline;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.airline.MemberAirlinePointDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MemberAirlinePointMapper extends BaseMapperX<MemberAirlinePointDO> {
default MemberAirlinePointDO selectByAccountId(Long accountId) {
return selectOne(new LambdaQueryWrapperX<MemberAirlinePointDO>()
.eq(MemberAirlinePointDO::getAccountId, accountId));
}
}
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.member.dal.mysql.interaction;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.interaction.MemberInteractionDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberInteractionMapper extends BaseMapperX<MemberInteractionDO> {
default PageResult<MemberInteractionDO> selectPageByReceiver(Long receiverId, PageParam pageParam) {
return selectPage(pageParam, new LambdaQueryWrapperX<MemberInteractionDO>()
.eq(MemberInteractionDO::getReceiverId, receiverId)
.orderByDesc(MemberInteractionDO::getId));
}
default Long selectUnreadCount(Long receiverId) {
return selectCount(new LambdaQueryWrapperX<MemberInteractionDO>()
.eq(MemberInteractionDO::getReceiverId, receiverId)
.eq(MemberInteractionDO::getReadStatus, false));
}
default MemberInteractionDO selectLatest(Long receiverId) {
List<MemberInteractionDO> list = selectList(new LambdaQueryWrapperX<MemberInteractionDO>()
.eq(MemberInteractionDO::getReceiverId, receiverId)
.orderByDesc(MemberInteractionDO::getId)
.last("LIMIT 1"));
return list.isEmpty() ? null : list.get(0);
}
default int markAllRead(Long receiverId) {
return update(null, new LambdaUpdateWrapper<MemberInteractionDO>()
.set(MemberInteractionDO::getReadStatus, true)
.eq(MemberInteractionDO::getReceiverId, receiverId)
.eq(MemberInteractionDO::getReadStatus, false));
}
}
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.member.dal.mysql.push;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.push.MemberPushDeviceDO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MemberPushDeviceMapper extends BaseMapperX<MemberPushDeviceDO> {
default MemberPushDeviceDO selectByRegistrationId(String registrationId) {
return selectOne(MemberPushDeviceDO::getRegistrationId, registrationId);
}
default List<MemberPushDeviceDO> selectListByMemberId(Long memberId) {
return selectList(new LambdaQueryWrapperX<MemberPushDeviceDO>()
.eq(MemberPushDeviceDO::getMemberId, memberId)
.orderByDesc(MemberPushDeviceDO::getId));
}
/**
* 物理删除,避免逻辑删除后 registration_id 唯一索引冲突。
*/
@Delete("DELETE FROM member_push_device WHERE id = #{id}")
int physicalDeleteById(@Param("id") Long id);
}
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.member.dal.mysql.rideplan;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanJoinDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberRidePlanJoinMapper extends BaseMapperX<MemberRidePlanJoinDO> {
default List<MemberRidePlanJoinDO> selectListByPlanId(Long planId) {
return selectList(new LambdaQueryWrapperX<MemberRidePlanJoinDO>()
.eq(MemberRidePlanJoinDO::getPlanId, planId));
}
default MemberRidePlanJoinDO selectByPlanIdAndUserId(Long planId, Long userId) {
return selectOne(new LambdaQueryWrapperX<MemberRidePlanJoinDO>()
.eq(MemberRidePlanJoinDO::getPlanId, planId)
.eq(MemberRidePlanJoinDO::getUserId, userId));
}
default List<MemberRidePlanJoinDO> selectListByUserId(Long userId) {
return selectList(new LambdaQueryWrapperX<MemberRidePlanJoinDO>()
.eq(MemberRidePlanJoinDO::getUserId, userId));
}
}
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.member.dal.mysql.rideplan;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRidePlanPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRidePlanDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
@Mapper
public interface MemberRidePlanMapper extends BaseMapperX<MemberRidePlanDO> {
/**
* 发车局分页(免登录可查):只查未下架、未过期的,按集合时间升序
*/
default PageResult<MemberRidePlanDO> selectPage(AppRidePlanPageReqVO reqVO) {
// 注意:LambdaQueryWrapperX 的 ifPresent 系列须在原生 eq/ge 之前调用,
// 原生方法返回值会退化为 LambdaQueryWrapper,链式后续找不到 ifPresent
LambdaQueryWrapperX<MemberRidePlanDO> wrapper = new LambdaQueryWrapperX<MemberRidePlanDO>()
.eqIfPresent(MemberRidePlanDO::getCity, reqVO.getCity())
.likeIfPresent(MemberRidePlanDO::getRouteName, reqVO.getKeyword());
wrapper.eq(MemberRidePlanDO::getStatus, 0);
wrapper.ge(MemberRidePlanDO::getMeetTime, LocalDateTime.now());
if (reqVO.getTag() != null && !reqVO.getTag().isEmpty()) {
wrapper.apply("FIND_IN_SET({0}, tags) > 0", reqVO.getTag());
}
wrapper.orderByAsc(MemberRidePlanDO::getMeetTime);
return selectPage(reqVO, wrapper);
}
default MemberRidePlanDO selectByIdAndStatus(Long id, Integer status) {
return selectOne(new LambdaQueryWrapperX<MemberRidePlanDO>()
.eq(MemberRidePlanDO::getId, id)
.eq(MemberRidePlanDO::getStatus, status));
}
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.member.dal.mysql.rideplan;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideRecordLikeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MemberRideRecordLikeMapper extends BaseMapperX<MemberRideRecordLikeDO> {
default MemberRideRecordLikeDO selectByRecordIdAndUserId(Long recordId, Long userId) {
return selectOne(new LambdaQueryWrapperX<MemberRideRecordLikeDO>()
.eq(MemberRideRecordLikeDO::getRecordId, recordId)
.eq(MemberRideRecordLikeDO::getUserId, userId));
}
default List<MemberRideRecordLikeDO> selectListByUserIdAndRecordIds(Long userId, Collection<Long> recordIds) {
return selectList(new LambdaQueryWrapperX<MemberRideRecordLikeDO>()
.eq(MemberRideRecordLikeDO::getUserId, userId)
.in(MemberRideRecordLikeDO::getRecordId, recordIds));
}
}
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.member.dal.mysql.rideplan;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.controller.app.rideplan.vo.AppRideRecordPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideRecordDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface MemberRideRecordMapper extends BaseMapperX<MemberRideRecordDO> {
/**
* 战报墙分页(免登录可查):只查正常状态,按发布时间倒序
*/
default PageResult<MemberRideRecordDO> selectPage(AppRideRecordPageReqVO reqVO) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eqIfPresent(MemberRideRecordDO::getUserId, reqVO.getUserId())
.eqIfPresent(MemberRideRecordDO::getRoadName, reqVO.getRoadName());
wrapper.eq(MemberRideRecordDO::getStatus, 0);
wrapper.orderByDesc(MemberRideRecordDO::getId);
return selectPage(reqVO, wrapper);
}
/**
* 我的历史骑行(分页)
*/
default PageResult<MemberRideRecordDO> selectMyPage(Long userId, AppRideRecordPageReqVO reqVO) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eq(MemberRideRecordDO::getUserId, userId)
.eq(MemberRideRecordDO::getStatus, 0);
wrapper.orderByDesc(MemberRideRecordDO::getId);
return selectPage(reqVO, wrapper);
}
/**
* 弯道天梯榜:取最大压弯角度最高的前 N 条(有弯道名的跑山记录)
*/
default List<MemberRideRecordDO> selectRank(String roadName, String cornerName, int limit) {
LambdaQueryWrapperX<MemberRideRecordDO> wrapper = new LambdaQueryWrapperX<MemberRideRecordDO>()
.eqIfPresent(MemberRideRecordDO::getRoadName, roadName)
.eqIfPresent(MemberRideRecordDO::getCornerName, cornerName);
wrapper.eq(MemberRideRecordDO::getStatus, 0);
wrapper.isNotNull(MemberRideRecordDO::getCornerName);
wrapper.ne(MemberRideRecordDO::getCornerName, "");
wrapper.orderByDesc(MemberRideRecordDO::getMaxLean);
wrapper.last("LIMIT " + limit);
return selectList(wrapper);
}
default MemberRideRecordDO selectByIdAndStatus(Long id, Integer status) {
return selectOne(new LambdaQueryWrapperX<MemberRideRecordDO>()
.eq(MemberRideRecordDO::getId, id)
.eq(MemberRideRecordDO::getStatus, status));
}
/**
* 批量查一批骑手的正常状态战报(用于列表/主页骑行数据统计)
*/
default List<MemberRideRecordDO> selectNormalByUserIds(Collection<Long> userIds) {
return selectList(new LambdaQueryWrapperX<MemberRideRecordDO>()
.in(MemberRideRecordDO::getUserId, userIds)
.eq(MemberRideRecordDO::getStatus, 0));
}
}
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.member.dal.mysql.rideplan;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.dal.dataobject.rideplan.MemberRideTagDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MemberRideTagMapper extends BaseMapperX<MemberRideTagDO> {
default List<MemberRideTagDO> selectListByStatus(Integer status) {
return selectList(new LambdaQueryWrapperX<MemberRideTagDO>()
.eq(MemberRideTagDO::getStatus, status)
.orderByAsc(MemberRideTagDO::getSort));
}
default MemberRideTagDO selectByName(String name) {
return selectOne(new LambdaQueryWrapperX<MemberRideTagDO>()
.eq(MemberRideTagDO::getName, name));
}
}
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more