diff --git a/HANDOVER.md b/HANDOVER.md index df5a343..4850942 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -1,95 +1,285 @@ -# 后端项目交接文档 +# as-daycard-openapi 项目交接文档 -## 项目信息 -- 路径:/Users/zhaoguodong/Desktop/as-daycard-openapi -- 启动:`./start.sh -Dspring-boot.run.profiles=dev`(端口8084,JDK17路径在start.sh里) -- 本地打包:`export JAVA_HOME=/Users/zhaoguodong/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home && mvn clean package -DskipTests` -- MySQL:/opt/homebrew/Cellar/mysql@8.0/8.0.42/bin/mysql -uroot -pr2356510 dev-0418 -- 接口文档:API_DOC.md(完整)+ THIRD_PARTY_API.md(第三方对接) -- DDL:db/full-schema.sql(52个表,完整表结构,从dev-0418导出) +## 一、项目概述 -## 云服务器部署 -- 服务器:宝塔面板,IP见用户确认 -- 数据库:jstopenapi(已建库+导入full-schema.sql+schedule-timeout-task.sql) -- MySQL权限:设为"所有人"(允许Docker容器通过host.docker.internal访问) -- Docker部署:docker-compose.yml已配好(DB_URL/REDIS_HOST用host.docker.internal,JWT/API签名密钥已生成) -- application-prod.yml:已创建(和dev相同配置,环境变量由docker-compose传入) -- Dockerfile:已改为multi-stage(JDK17),但服务器上用的是简单版(本地打包jar上传+COPY) -- 部署方式:本地mvn打包 -> scp上传jar -> docker-compose build/up -- **已知问题**:MySQL的max_connect_errors封锁Docker容器IP,需 `mysql -uroot -p -e "FLUSH HOSTS;"` 解除后重启容器 -- **部署状态**:jar已上传,application-prod.yml已包含,正在解决MySQL连接封锁问题 +天卡(24小时制视频权益卡)开放API平台,为酒店电视设备提供天卡购买、支付、激活、权益管理的完整能力。设备端通过本平台完成微信支付购买天卡,支付成功后异步调用橘视厅接口激活权益,设备获取 `thirdCardNo` 凭证后直连橘视厅SDK播放内容。 -## 已完成的功能 +**技术栈**: Java 8 / Spring Boot 2.6.6 / MyBatis-Plus / MySQL / Redis / Shiro + JWT / Quartz / wechatpay-java SDK 0.2.14 -### 1. 橘视厅媒资接口(/manage/jetsen-media/**,JWT鉴权) -- catalog-tree/list/search/export/nav/tags -- 栏目树(一级+二级),list必须用二级栏目id(如472=3221条) -- orderinfo:"1"必须带 +**部署**: Docker (eclipse-temurin:17-jre-alpine),端口 8084,Spring Profile = prod -### 2. 第三方开放接口(/openapi/partner/**,appKey签名鉴权) -- PartnerSignFilter:MD5(appKey+timestamp+nonce+appSecret).toUpperCase(),5分钟有效+nonce防重放 -- 媒资接口(/openapi/partner/media/):package/catalog-tree/list/search -- 三级授权:CONTENT(单部内容)>CATALOG(栏目)>FULL(全量),按open_api_content_package配置判定 -- 天卡代销(/openapi/partner/card/):consume(消耗天卡绑定设备)/stock(库存) -- 调用日志:open_api_call_log +--- -### 3. 代销模式 -- 采购:POST /manage/partner-card/purchase(平台卡池UNUSED->RESERVED,记入partner池子AVAILABLE) -- 消耗:POST /openapi/partner/card/consume(partner池子取1张,绑定设备,RESERVED->USED,异步激活橘视厅) -- 回退:POST /manage/partner-card/revert(AVAILABLE的天卡回退,RESERVED->UNUSED,partner池子DISABLED) -- 免activationCode init:/api/jst/init不传activationCode时按设备标识查找已注册设备(代销模式) -- APK流程:consume消耗天卡 -> APK init(免activationCode) -> checkBenefits拿thirdCardNo -> 播放 +## 二、组织架构与数据隔离 -### 4. 内容包管理(/manage/content-package/**,JWT鉴权) -- CRUD + fetch-contents(从橘视厅拉内容预览)+ save-from-selection(勾选内容一键保存) -- info/info-by-partner返回contentList(解析后的内容数组) -- save-from-selection支持传id覆盖更新 +### 四层组织体系 -### 5. 接口风格统一 -- 所有接口返回:{code:0, msg:"success", data:{...}} -- APK设备接口原code:200已改0,sign从顶层移入data -- UI接口原retCode:"00000000"已改{code:0,msg,data} -- 管理后台R.ok().put()全部改R.dataSuccess()(数据在data内) -- 路径统一:/sys/管理接口移到/manage/(仅login/refresh/logout保留/sys/) -- 合作伙伴管理从/openapi/partner移到/manage/partner(JWT鉴权) -- 删除冗余DayCardPoolController(和DayCardManageController冲突) +``` +平台(Platform) > 渠道(Channel) > 代理商(Agent/SERVICE_PROVIDER) > 门店(Store) +``` -### 6. 审计日志 -- audit_log表已重建(字段对齐AuditLogEntity) -- 46个@AuditAction审计点(资金/资产/设备/组织写操作) -- AuditLogAspect切面自动记录 +- 每个组织节点有 `mpath` 物化路径(如 `1.2.3.`),用于快速判断层级关系 +- 用户通过 `sys_user.org_id` 关联到某个组织节点 +- `org_id = NULL` 为超级管理员,不限制数据范围 -## 数据库关键表 -- 合作伙伴:open_api_partner(appKey/appSecret) -- 内容包:open_api_content_package(partnerId/catalogIds/contentCodes) -- partner天卡池:open_api_partner_card_pool(partnerId/poolId/cardCode/status) -- 调用日志:open_api_call_log -- 审计日志:audit_log(已重建) -- 天卡池:day_card_pool(UNUSED->RESERVED->USED->REUSABLE) -- 设备:device(含androidId/manufacturer/model/brand扩展字段) +### 数据隔离机制 -## 测试验证状态 -- ✅ 媒资接口(4个)+ appKey签名鉴权 + 三级授权过滤 -- ✅ 采购天卡 + 消耗天卡 + 回退天卡 -- ✅ 内容包配置(fetch-contents + save-from-selection) -- ✅ 禁用合作伙伴 -> 鉴权失败 -- ✅ 删除内容包 -> FULL全量fallback -- ✅ 管理后台前端已适配新路径和返回格式 +- **隔离工具**: `SysOrgService.applyBusinessOrgScope(wrapper, orgColumn, operatorId)` —— 向业务表 QueryWrapper 注入子查询,按 `sys_org.mpath` 前缀匹配过滤 +- **权限校验**: `SysOrgService.isOrgInScope(orgId, operatorId)` —— 判断目标组织是否在操作人范围内 +- **已接入隔离的模块**: 设备、订单、退款、统计、定价、支付配置、激活码、用户管理 +- **平台级资源不隔离**: 天卡池(全局库存)、系统菜单 -## 待处理事项 -1. **服务器部署**:解决MySQL连接封锁后重启Docker容器,验证启动成功 -2. **直营模式**(方式2):EPG调开放订单接口 -> 微信支付 -> 自动发卡(未开发) -3. **旧代码清理**:DayCardSettleController(旧分账)/MediaController(桩)/ContractServiceImpl(桩)等废弃代码 -4. **Nginx反代+HTTPS**:宝塔配反代,第三方用域名调用 -5. **前端菜单**:合作伙伴管理/内容包管理/天卡池管理的菜单页面 +--- -## 关键配置 -- docker-compose.yml:DB_URL=jstopenapi, REDIS_HOST=host.docker.internal, JWT_SECRET=2F064BA10037477FBB3A1BF9D508AE96, API_SIGN_SECRET=5F2A2DD8BFBA4DA1B87EFBB405671B3D -- application-prod.yml:和dev相同,环境变量传入 -- Druid wall filter:dev和prod都禁用(不兼容SKIP LOCKED) -- Lombok:1.18.24,必须用JDK17编译(JDK21+不兼容) +## 三、核心业务流程 -## 合作伙伴测试数据(本地dev-0418) -- 华住酒店:partnerId=0f91ea23961e4983, appKey=b9f9542e1e044466, appSecret=d117d67429b941c1bc0e0f6044e39d62 -- 测试合作伙伴:partnerId=1, appKey=71df57e7e47a4a88, appSecret=512f147432124bb48b313365ee31b87b -- 内容包:id=3, name=高分3部, catalogIds=472, contentCount=3, CONTENT级别 +### 3.1 设备初始化与鉴权 + +``` +设备 App 启动 + -> POST /api/jst/init (body: activationCode + mac/deviceSn/deviceUid) + -> 后端创建/更新设备记录,生成 sign + -> 返回 { sign, deviceId } + -> 后续所有请求在 header 携带 sign +``` + +- **激活码模式**: 设备通过管理后台生成的门店激活码初始化,绑定到门店组织 +- **代销模式**: 第三方合作伙伴的设备免激活码,通过设备标识匹配已注册设备 +- **sign 机制**: 后端 `ApiDeviceSignFilter` 验证 sign,解析出 deviceId 放入 request attribute。sign 有时效,可通过 `heartBeat` 续期或 `refreshSign` 刷新 + +### 3.2 天卡购买流程 + +``` +1. 设备查询权益 -> POST /api/jst/checkBenefits -> UNBOUND(无权益) +2. 设备查询价格 -> POST /api/jst/price -> 返回价格 +3. 设备创建订单 -> POST /api/jst/daycard/create + -> Redis 防重锁(按 deviceId) + -> 解析设备组织链路(设备 -> 门店 -> 渠道 -> 代理商) + -> 3层定价匹配(门店 > 渠道 > 平台默认) + -> 插入订单(状态 PENDING) + -> 调微信 Native 下单,获取 code_url + -> SKIP LOCKED 抢卡(UNUSED -> RESERVED) + -> 返回 { orderNo, codeUrl, amount, expireAt } +4. App 用 codeUrl 生成二维码,用户扫码支付 +5. 用户取消支付 -> POST /api/jst/daycard/cancel/{orderNo} + -> 校验订单归属 -> 微信关单 -> 释放卡(RESERVED -> UNUSED)-> 订单 CANCELLED -> 清理 Redis 锁 +6. 用户支付成功 -> 微信回调 -> 异步激活 +``` + +### 3.3 支付回调与异步激活 + +``` +微信支付回调 -> POST /openapi/payment/wechat/notify + -> 验签(从 header 提取 Wechatpay-Serial/Nonce/Signature/Timestamp,HttpServletRequest.getHeader() 大小写不敏感) + -> 解密获取交易信息 + -> 幂等检查(订单已 PAID 则直接返回) + -> 乐观锁更新订单 PAID + 卡状态 RESERVED -> USED + -> 异步激活(@Async): + 1. jushitingApiService.getToken(device, thirdCardNo) -> 获取 token + memberCode + 2. jushitingApiService.activateCard(deviceId, cardCode, memberCode, token) -> 激活卡密 + 3. jushitingApiService.queryVipExpireTime(deviceId, thirdCardNo) -> 查询VIP到期时间 + 4. 更新 day_card_pool: expire_at + activate_status=SUCCESS + -> 清理 Redis 防重锁 +``` + +### 3.4 橘视厅权益映射 + +``` +关键标识 thirdCardNo: + - 卡密导入时自动生成(D + 24位随机字符) + - 存储在 day_card_pool.third_card_no + - 作为橘视厅的 uid 和 mac 参数(mac = thirdCardNo + "_bul_" + businessLine) + - 设备通过 checkBenefits 获取 thirdCardNo + - 设备用 thirdCardNo 直连橘视厅获取 token,激活卡密,获得 VIP 权益 + +设备与卡的关联: + day_card_pool.device_id -> device.id -> device.mac/device_sn/device_uid + 解绑时清除 device_id,卡状态保持 USED(终态不可复用) +``` + +### 3.5 卡状态流转 + +``` +UNUSED -> RESERVED -> USED(终态) + 导入 下单抢卡 支付成功 + +取消/超时: RESERVED -> UNUSED(释放回池) +退款: USED -> UNUSED(清除 device_id/order_no/expire_at/activate_status) +解绑: USED 保持不变,仅清除 device_id +``` + +### 3.6 代销模式(合作伙伴) + +``` +采购: 平台天卡池 UNUSED -> RESERVED,写入 partner_card_pool(status=AVAILABLE) +消耗: POST /openapi/partner/card/consume + -> partner 池取1张 AVAILABLE 卡 + -> day_card_pool RESERVED -> USED + device_id + -> partner_card_pool AVAILABLE -> CONSUMED + -> 异步激活(同上) + -> App 后续用同一设备标识调 init + checkBenefits +回退: partner_card_pool AVAILABLE -> DISABLED,平台卡 RESERVED -> UNUSED +``` + +### 3.7 分账(需微信商户号开通分账权限) + +``` +支付成功后触发 executeSharing(orderNo): + -> 匹配分润规则(全局/渠道/代理商/门店四级) + -> 计算各方分账金额(比例或固定,非平台方总比例 ≤ 30%) + -> 创建 profit_sharing_order + profit_sharing_detail + -> 调微信 createProfitSharing API(设置 unfreezeUnsplit=true 自动解冻剩余) + -> 调微信 finishProfitSharing 完结分账 + +注意: 当前分账标记(SettleInfo.profitSharing=true)已注释,需商户号开通分账权限后取消注释 +``` + +--- + +## 四、接口体系 + +### 4.1 设备端接口(/api/jst/,sign 鉴权) + +| 接口 | 说明 | +|------|------| +| POST /api/jst/init | 设备初始化,返回 sign | +| POST /api/jst/checkBenefits | 查询天卡权益,返回 thirdCardNo | +| POST /api/jst/price | 查询天卡价格 | +| POST /api/jst/daycard/create | 创建天卡订单 | +| GET /api/jst/daycard/status/{orderNo} | 查询支付状态 | +| POST /api/jst/daycard/cancel/{orderNo} | 取消订单 | +| POST /api/jst/heartBeat | 心跳续期 | +| POST /api/jst/refreshSign | 刷新 sign | + +### 4.2 管理后台接口(/manage/,JWT 鉴权) + +覆盖:系统管理(用户/角色/菜单/组织/门店)、设备管理、天卡池/订单/退款/激活码、定价策略、支付配置、分账规则/记录、统计概览、合作伙伴管理、媒资管理等,共约 100+ 个接口。 + +### 4.3 合作伙伴接口(/openapi/partner/,appKey/appSecret 签名鉴权) + +| 接口 | 说明 | +|------|------| +| POST /openapi/partner/card/consume | 消耗天卡(代销) | +| GET /openapi/partner/card/stock | 查询库存 | +| GET /openapi/partner/media/* | 媒资接口(内容包/栏目树/搜索等) | + +### 4.4 微信回调接口(/openapi/payment/,无鉴权) + +| 接口 | 说明 | +|------|------| +| POST /openapi/payment/wechat/notify | 支付回调 | +| POST /openapi/payment/refund/notify | 退款回调 | +| GET /openapi/payment/query/{orderNo} | 查询支付状态 | + +### 4.5 统一响应结构 + +```json +{ "code": 0, "msg": "", "data": {} } +``` +- code=0 成功,code!=0 失败(500 业务错误,403 权限错误) +- data 始终存在,列表分页用 `data.page`,详情用 `data.{业务字段}` + +--- + +## 五、定价体系 + +三层维度优先级匹配:门店配置价 > 渠道配置价 > 平台默认价 + +| 配置层级 | channel_org_id | store_org_id | +|---------|---------------|-------------| +| 平台默认 | NULL | NULL | +| 渠道级 | 具体值 | NULL | +| 门店级 | NULL | 具体值 | + +--- + +## 六、定时任务 + +| 任务 | 说明 | +|------|------| +| DayCardOrderTimeoutTask | 取消超时未支付订单(15分钟),释放卡 | +| DayCardActivateRetryTask | 重试激活失败的天卡 | +| ProfitSharingQueryTask | 查询分账状态,更新分账单 | + +--- + +## 七、橘视厅平台对接 + +### 平台级凭证(用于内容/UI接口,非设备级) + +``` +SP_TOKEN = "9006874392099a89506142c6df9e3314" +KEY = "alltvjs" +PORTAL_CODE = "PCODE-JC" +sign = MD5(sptoken + timeshift + portalcode + key).toUpperCase() +``` + +App 用此凭证直连橘视厅 `/IPTVPortalInterface/` 下的接口(topButton、gettagslist、getcontentlist、search 等),获取内容和 UI。**不经过我们后端**。 + +### 设备级权益凭证(thirdCardNo) + +- 由后端在卡密导入时自动生成 +- 设备通过 `checkBenefits` 获取 +- 用于橘视厅免密登录(getToken)和卡密激活(activateCard) +- `getToken` 的 uid 和 mac 参数都基于 thirdCardNo 构造,设备实体不传给橘视厅 + +### 橘视厅 API 基地址 + +- 正式环境: `https://api.jetsenx.huashi.tv/` +- 测试环境: `https://external.jetsenx.huashi.tv/` +- 媒资接口: `https://servhub-ep03.future.jetsentv.com/` + +--- + +## 八、微信支付配置 + +- 支持多商户号配置(DB `wechat_pay_config` 表,四级优先级:门店 > 代理商 > 渠道 > 全局) +- DB 配置优先,回退到 yml 环境变量 +- SDK 配置实例按 商户号+证书序列号+密钥+私钥 缓存,配置变更自动重建 +- 回调验签支持多商户号(遍历所有 ACTIVE 配置逐个尝试解析) + +--- + +## 九、待办与注意事项 + +1. **分账功能**: 代码已实现但分账标记(`SettleInfo.profitSharing=true`)已注释,需微信商户号开通分账权限后取消注释(WechatPayServiceImpl 和 DayCardOrderServiceImpl.payCallback 中) +2. **退款SDK**: `WechatPayServiceImpl.refund()` 目前是骨架实现(TODO 接入 wechatpay-java RefundService) +3. **设备标识改造**: 当前卡池用 `device_id`(Long, 自增) 关联设备,计划改为 `device_uid`(String) 关联,已搁置 +4. **天卡池不隔离**: 天卡池为平台级库存资源,不按组织隔离,所有管理员可见 +5. **媒资接口分页不统一**: 3个媒资相关端点用 `data.list` + `data.totalCount` 平铺式分页,与其他接口的 `data.page` 不同 +6. **支付接口路径**: 当前支付接口在 `/openapi/payment/` 下,计划迁移到 `/payment/` 与第三方接口区分(未实施) +7. **密码哈希**: Shiro `Sha256Hash(password, salt)` 哈希顺序为 `salt + password`(不是 password + salt) + +--- + +## 十、数据库与部署 + +### 数据库 +- MySQL 8.0,数据库名 `jstopenapi` +- 完整建表脚本: `db/full-schema.sql` +- 初始化数据: `db/init-sys-data.sql`(菜单 + 角色 + 超管用户 admin/admin) +- 逻辑删除: `delete_flag` 字段(UNDEL/DEL) + +### Redis +- 用于:设备 sign 缓存、防重锁、橘视厅 token 缓存、激活锁 +- Key 前缀: `daycard:device:`(订单防重)、`api:thirdparty:activate:`(激活相关) + +### 部署 +```bash +# 构建 +mvn clean package -DskipTests + +# Docker 部署 +docker-compose up -d + +# 关键环境变量(docker-compose.yml 中配置) +SPRING_PROFILES_ACTIVE=prod +DB_URL / DB_USERNAME / DB_PASSWORD +REDIS_HOST / REDIS_PORT / REDIS_PASSWORD +JWT_SECRET / API_SIGN_SECRET +WECHAT_PAY_* # 微信支付配置(或通过 DB wechat_pay_config 表配置) +``` + +### 超管账号 +- 用户名: admin / 密码: admin +- 新增超管: wangrui / 密码: Admin@2026. diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardActivationCodeController.java b/src/main/java/io/as/modules/daycard/controller/DayCardActivationCodeController.java index 055e47d..6c1e15a 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardActivationCodeController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardActivationCodeController.java @@ -5,6 +5,7 @@ import io.as.common.exception.ASException; import io.as.common.utils.R; import io.as.modules.daycard.entity.DayCardActivationCodeEntity; import io.as.modules.daycard.service.DayCardActivationCodeService; +import io.as.modules.sys.controller.AbstractController; import io.as.modules.sys.entity.SysOrgEntity; import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; @@ -27,7 +28,7 @@ import java.util.Map; @Api(tags = "天卡设备激活码") @RestController @RequestMapping("/manage/daycard/activation-code") -public class DayCardActivationCodeController { +public class DayCardActivationCodeController extends AbstractController { @Autowired private DayCardActivationCodeService dayCardActivationCodeService; @@ -41,6 +42,9 @@ public class DayCardActivationCodeController { @ApiOperation("按门店查询激活码列表") @GetMapping("/list/{storeOrgId}") public R listByStore(@PathVariable("storeOrgId") Long storeOrgId) { + if (!sysOrgService.isOrgInScope(storeOrgId, getUserId())) { + throw new ASException("无权限操作该门店的数据"); + } List list = dayCardActivationCodeService.listByStoreOrgId(storeOrgId); List> result = new ArrayList<>(); for (DayCardActivationCodeEntity entity : list) { @@ -61,6 +65,9 @@ public class DayCardActivationCodeController { if (request.getStoreOrgId() == null) { throw new ASException("门店组织ID不能为空"); } + if (!sysOrgService.isOrgInScope(request.getStoreOrgId(), getUserId())) { + throw new ASException("无权限操作该门店的数据"); + } // 校验门店存在 SysOrgEntity storeOrg = sysOrgService.getById(request.getStoreOrgId()); if (storeOrg == null) { @@ -70,9 +77,7 @@ public class DayCardActivationCodeController { Long channelOrgId = resolveChannelOrgId(storeOrg); DayCardActivationCodeEntity entity = dayCardActivationCodeService.generateForStore( request.getStoreOrgId(), channelOrgId, request.getRemark()); - Map data = new HashMap<>(); - data.put("data", toMap(entity)); - return R.dataSuccess(data); + return R.dataSuccess(toMap(entity)); } /** @@ -82,12 +87,17 @@ public class DayCardActivationCodeController { @AuditAction(value = "ACTCODE_REGENERATE", description = "重新生成激活码") @PostMapping("/{id}/regenerate") public R regenerate(@PathVariable("id") Long id) { + DayCardActivationCodeEntity entity = dayCardActivationCodeService.getById(id); + if (entity == null) { + throw new ASException("激活码不存在"); + } + if (!sysOrgService.isOrgInScope(entity.getStoreOrgId(), getUserId())) { + throw new ASException("无权限操作该门店的数据"); + } String newCode = dayCardActivationCodeService.regenerateCode(id); Map item = new HashMap<>(); item.put("code", newCode); - Map data = new HashMap<>(); - data.put("data", item); - return R.dataSuccess(data); + return R.dataSuccess(item); } /** @@ -97,8 +107,15 @@ public class DayCardActivationCodeController { @AuditAction(value = "ACTCODE_UPDATE_STATUS", description = "修改激活码状态") @PutMapping("/{id}/status") public R updateStatus(@PathVariable("id") Long id, @RequestBody UpdateStatusRequest request) { + DayCardActivationCodeEntity entity = dayCardActivationCodeService.getById(id); + if (entity == null) { + throw new ASException("激活码不存在"); + } + if (!sysOrgService.isOrgInScope(entity.getStoreOrgId(), getUserId())) { + throw new ASException("无权限操作该门店的数据"); + } dayCardActivationCodeService.updateStatus(id, request.getStatus()); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -108,8 +125,15 @@ public class DayCardActivationCodeController { @AuditAction(value = "ACTCODE_DELETE", description = "删除激活码") @DeleteMapping("/{id}") public R delete(@PathVariable("id") Long id) { + DayCardActivationCodeEntity entity = dayCardActivationCodeService.getById(id); + if (entity == null) { + throw new ASException("激活码不存在"); + } + if (!sysOrgService.isOrgInScope(entity.getStoreOrgId(), getUserId())) { + throw new ASException("无权限操作该门店的数据"); + } dayCardActivationCodeService.delete(id); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } // ==================== 内部方法 ==================== diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardManageController.java b/src/main/java/io/as/modules/daycard/controller/DayCardManageController.java index 6edfca2..acce05d 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardManageController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardManageController.java @@ -9,6 +9,10 @@ import io.as.modules.daycard.entity.DayCardPoolEntity; import io.as.modules.daycard.service.DayCardOrderService; import io.as.modules.daycard.service.DayCardPoolService; import io.as.modules.daycard.vo.DayCardPoolStatsVo; +import io.as.modules.device.entity.DeviceEntity; +import io.as.modules.device.service.DeviceService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -24,7 +28,7 @@ import java.util.Map; @RestController @RequestMapping("/manage/daycard") @RequiresAuthentication -public class DayCardManageController { +public class DayCardManageController extends AbstractController { @Autowired private DayCardPoolService dayCardPoolService; @@ -32,10 +36,17 @@ public class DayCardManageController { @Autowired private DayCardOrderService dayCardOrderService; + @Autowired + private SysOrgService sysOrgService; + + @Autowired + private DeviceService deviceService; + // ===================== 天卡池 ===================== /** * 天卡池列表 + *

天卡池为平台级资源,不按组织隔离

*/ @GetMapping("/pool/list") public R poolList(@RequestParam Map params) { @@ -94,8 +105,7 @@ public class DayCardManageController { */ @GetMapping("/order/list") public R orderList(@RequestParam Map params) { - Long partnerId = parsePartnerId(params); - PageUtils page = dayCardOrderService.queryPage(params, partnerId); + PageUtils page = dayCardOrderService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -108,6 +118,10 @@ public class DayCardManageController { */ @GetMapping("/benefits/{deviceId}") public R deviceBenefits(@PathVariable Long deviceId) { + DeviceEntity device = deviceService.getById(deviceId); + if (device != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限查看该设备"); + } DayCardPoolEntity pool = dayCardOrderService.getDeviceBenefits(deviceId); Map item = new HashMap<>(); if (pool != null) { @@ -118,9 +132,7 @@ public class DayCardManageController { } else { item.put("hasBenefits", false); } - Map data = new HashMap<>(); - data.put("data", item); - return R.dataSuccess(data); + return R.dataSuccess(item); } /** @@ -130,8 +142,14 @@ public class DayCardManageController { @AuditAction(value = "DAYCARD_DEVICE_UNBIND", description = "解绑设备天卡权益") @PostMapping("/device/unbind") public R unbindDevice(@RequestParam Long deviceId) { + DeviceEntity device = deviceService.getById(deviceId); + if (device != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限操作该设备"); + } dayCardOrderService.unbindDevice(deviceId); - return R.ok("设备解绑成功"); + Map data = new HashMap<>(); + data.put("message", "设备解绑成功"); + return R.dataSuccess(data); } /** @@ -141,8 +159,14 @@ public class DayCardManageController { @AuditAction(value = "DAYCARD_DEVICE_UNBIND_ORG", description = "设备组织解绑") @PostMapping("/device/unbind-org") public R unbindDeviceOrg(@RequestParam Long deviceId) { + DeviceEntity device = deviceService.getById(deviceId); + if (device != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限操作该设备"); + } dayCardOrderService.unbindDeviceOrg(deviceId); - return R.ok("设备组织解绑成功"); + Map data = new HashMap<>(); + data.put("message", "设备组织解绑成功"); + return R.dataSuccess(data); } // ===================== 内部方法 ===================== diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardOrderController.java b/src/main/java/io/as/modules/daycard/controller/DayCardOrderController.java index b844bec..5942245 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardOrderController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardOrderController.java @@ -5,6 +5,7 @@ import io.as.common.utils.PageUtils; import io.as.common.utils.R; import io.as.modules.daycard.entity.DayCardOrderEntity; import io.as.modules.daycard.service.DayCardOrderService; +import io.as.modules.sys.controller.AbstractController; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -20,7 +21,7 @@ import java.util.Map; @Api(tags = "天卡订单") @RestController @RequestMapping("/manage/daycard") -public class DayCardOrderController { +public class DayCardOrderController extends AbstractController { @Autowired private DayCardOrderService dayCardOrderService; @@ -31,8 +32,7 @@ public class DayCardOrderController { @ApiOperation("分页查询天卡订单") @GetMapping("/list") public R list(@RequestParam Map params) { - Long partnerId = parsePartnerId(params); - PageUtils page = dayCardOrderService.queryPage(params, partnerId); + PageUtils page = dayCardOrderService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -52,22 +52,4 @@ public class DayCardOrderController { data.put("order", order); return R.dataSuccess(data); } - - /** - * 从请求参数中解析 partnerId(兼容存量数据查询,新订单 partnerId 为 null) - */ - private Long parsePartnerId(Map params) { - Object val = params.get("partnerId"); - if (val == null) { - val = params.get("partner_id"); - } - if (val == null) { - return null; - } - try { - return Long.parseLong(String.valueOf(val)); - } catch (NumberFormatException e) { - return null; - } - } } diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardPriceConfigController.java b/src/main/java/io/as/modules/daycard/controller/DayCardPriceConfigController.java index 76c40f4..e310e4b 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardPriceConfigController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardPriceConfigController.java @@ -7,6 +7,8 @@ import io.as.common.utils.R; import io.as.modules.daycard.constants.DayCardConstants; import io.as.modules.daycard.entity.DayCardPriceConfigEntity; import io.as.modules.daycard.service.DayCardPriceConfigService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -24,18 +26,21 @@ import java.util.Map; @Api(tags = "天卡定价策略管理") @RestController @RequestMapping("/manage/price") -public class DayCardPriceConfigController { +public class DayCardPriceConfigController extends AbstractController { @Autowired private DayCardPriceConfigService dayCardPriceConfigService; + @Autowired + private SysOrgService sysOrgService; + /** * 分页查询定价策略(支持 partnerId/status 筛选) */ @ApiOperation("分页查询定价策略") @GetMapping("/list") public R list(@RequestParam Map params) { - PageUtils page = dayCardPriceConfigService.queryPage(params); + PageUtils page = dayCardPriceConfigService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -51,6 +56,7 @@ public class DayCardPriceConfigController { if (config == null) { throw new ASException("定价策略不存在"); } + checkPriceConfigOrgScope(config); Map data = new HashMap<>(); data.put("config", config); return R.dataSuccess(data); @@ -80,6 +86,7 @@ public class DayCardPriceConfigController { @AuditAction(value = "PRICE_SAVE", description = "新增定价策略") @PostMapping("/save") public R save(@RequestBody DayCardPriceConfigEntity config) { + checkPriceConfigOrgScope(config); Date now = new Date(); config.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL); config.setCreatedAt(now); @@ -88,7 +95,9 @@ public class DayCardPriceConfigController { config.setStatus("ACTIVE"); } dayCardPriceConfigService.save(config); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", config.getId()); + return R.dataSuccess(data); } /** @@ -101,9 +110,14 @@ public class DayCardPriceConfigController { if (config.getId() == null) { throw new ASException("id不能为空"); } + DayCardPriceConfigEntity existing = dayCardPriceConfigService.getById(config.getId()); + if (existing == null) { + throw new ASException("定价策略不存在"); + } + checkPriceConfigOrgScope(existing); config.setUpdatedAt(new Date()); dayCardPriceConfigService.updateById(config); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -118,12 +132,28 @@ public class DayCardPriceConfigController { } List list = dayCardPriceConfigService.listByIds( Arrays.asList(ids)); + for (DayCardPriceConfigEntity config : list) { + checkPriceConfigOrgScope(config); + } Date now = new Date(); for (DayCardPriceConfigEntity config : list) { config.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL); config.setUpdatedAt(now); } dayCardPriceConfigService.updateBatchById(list); - return R.ok(); + return R.dataSuccess(new HashMap<>()); + } + + private void checkPriceConfigOrgScope(DayCardPriceConfigEntity config) { + Long orgId = config.getStoreOrgId() != null ? config.getStoreOrgId() : config.getChannelOrgId(); + if (orgId != null) { + if (!sysOrgService.isOrgInScope(orgId, getUserId())) { + throw new ASException("无权限操作该组织的数据"); + } + } else { + if (sysOrgService.getUserOrgMpath(getUserId()) != null) { + throw new ASException("无权限操作平台默认配置"); + } + } } } diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardRefundController.java b/src/main/java/io/as/modules/daycard/controller/DayCardRefundController.java index 9e21707..dfe99b4 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardRefundController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardRefundController.java @@ -4,8 +4,12 @@ import io.as.common.annotation.AuditAction; import io.as.common.exception.ASException; import io.as.common.utils.PageUtils; import io.as.common.utils.R; +import io.as.modules.daycard.entity.DayCardOrderEntity; import io.as.modules.daycard.entity.DayCardRefundEntity; +import io.as.modules.daycard.service.DayCardOrderService; import io.as.modules.daycard.service.DayCardRefundService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang.StringUtils; @@ -21,18 +25,24 @@ import java.util.Map; @Api(tags = "天卡退款管理") @RestController @RequestMapping("/manage/refund") -public class DayCardRefundController { +public class DayCardRefundController extends AbstractController { @Autowired private DayCardRefundService dayCardRefundService; + @Autowired + private SysOrgService sysOrgService; + + @Autowired + private DayCardOrderService dayCardOrderService; + /** * 分页查询退款记录 */ @ApiOperation("分页查询退款记录") @GetMapping("/list") public R list(@RequestParam Map params) { - PageUtils page = dayCardRefundService.queryPage(params); + PageUtils page = dayCardRefundService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -48,6 +58,12 @@ public class DayCardRefundController { if (refund == null) { throw new ASException("退款记录不存在"); } + // 组织数据隔离:校验关联订单是否在操作人数据范围内 + DayCardOrderEntity order = dayCardOrderService.getByOrderNo(refund.getOrderNo()); + if (order != null && order.getStoreOrgId() != null + && !sysOrgService.isOrgInScope(order.getStoreOrgId(), getUserId())) { + throw new ASException("无权限查看该退款记录"); + } Map data = new HashMap<>(); data.put("refund", refund); return R.dataSuccess(data); @@ -66,6 +82,15 @@ public class DayCardRefundController { if (StringUtils.isBlank(orderNo)) { throw new ASException("orderNo不能为空"); } + // 组织数据隔离:校验订单是否在操作人数据范围内 + DayCardOrderEntity order = dayCardOrderService.getByOrderNo(orderNo); + if (order == null) { + throw new ASException("订单不存在: " + orderNo); + } + if (order.getStoreOrgId() != null + && !sysOrgService.isOrgInScope(order.getStoreOrgId(), getUserId())) { + throw new ASException("无权限操作该订单的退款"); + } String reason = params.get("reason"); DayCardRefundEntity refund = dayCardRefundService.refund(orderNo, reason); Map data = new HashMap<>(); diff --git a/src/main/java/io/as/modules/daycard/controller/DayCardSettleController.java b/src/main/java/io/as/modules/daycard/controller/DayCardSettleController.java index b83ee64..c8e9bd5 100644 --- a/src/main/java/io/as/modules/daycard/controller/DayCardSettleController.java +++ b/src/main/java/io/as/modules/daycard/controller/DayCardSettleController.java @@ -96,7 +96,7 @@ public class DayCardSettleController { throw new ASException("id不能为空"); } dayCardSettleRuleService.update(rule); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -106,7 +106,7 @@ public class DayCardSettleController { @PostMapping("/rule/delete") public R ruleDelete(@RequestBody Long[] ids) { dayCardSettleRuleService.delete(ids); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } // ==================== 分账记录 ==================== @@ -185,8 +185,6 @@ public class DayCardSettleController { throw new ASException("totalAmount必须大于0"); } Map result = profitCalcService.calculate(partnerId, channelOrgId, storeOrgId, totalAmount); - Map data = new HashMap<>(); - data.put("data", result); - return R.dataSuccess(data); + return R.dataSuccess(result); } } diff --git a/src/main/java/io/as/modules/daycard/controller/OrgSettlementAccountController.java b/src/main/java/io/as/modules/daycard/controller/OrgSettlementAccountController.java index fb093cf..97fde41 100644 --- a/src/main/java/io/as/modules/daycard/controller/OrgSettlementAccountController.java +++ b/src/main/java/io/as/modules/daycard/controller/OrgSettlementAccountController.java @@ -82,7 +82,7 @@ public class OrgSettlementAccountController { @PostMapping("/delete") public R delete(@RequestBody Long[] ids) { orgSettlementAccountService.delete(ids); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -93,6 +93,6 @@ public class OrgSettlementAccountController { @PostMapping("/sync/{id}") public R sync(@PathVariable("id") Long id) { orgSettlementAccountService.syncToWechat(id); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } } diff --git a/src/main/java/io/as/modules/daycard/controller/ProfitSharingRuleController.java b/src/main/java/io/as/modules/daycard/controller/ProfitSharingRuleController.java index 815f00d..5ce316e 100644 --- a/src/main/java/io/as/modules/daycard/controller/ProfitSharingRuleController.java +++ b/src/main/java/io/as/modules/daycard/controller/ProfitSharingRuleController.java @@ -94,7 +94,7 @@ public class ProfitSharingRuleController { throw new ASException("id不能为空"); } profitSharingRuleService.updateRule(rule); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -106,7 +106,7 @@ public class ProfitSharingRuleController { public R delete(@RequestBody Map body) { Long id = Long.parseLong(body.get("id").toString()); profitSharingRuleService.delete(id); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -245,8 +245,6 @@ public class ProfitSharingRuleController { result.put("details", details); result.put("nonPlatformTotalRatio", nonPlatformTotalRatio); result.put("warning", warning); - Map data = new HashMap<>(); - data.put("data", result); - return R.dataSuccess(data); + return R.dataSuccess(result); } } diff --git a/src/main/java/io/as/modules/daycard/service/DayCardOrderService.java b/src/main/java/io/as/modules/daycard/service/DayCardOrderService.java index 217cbc5..de06fb1 100644 --- a/src/main/java/io/as/modules/daycard/service/DayCardOrderService.java +++ b/src/main/java/io/as/modules/daycard/service/DayCardOrderService.java @@ -17,10 +17,10 @@ public interface DayCardOrderService extends IService { /** * 分页查询天卡订单 * - * @param params 查询参数 - * @param partnerId 合作商ID + * @param params 查询参数 + * @param operatorId 当前操作人userId(用于组织数据隔离) */ - PageUtils queryPage(Map params, Long partnerId); + PageUtils queryPage(Map params, Long operatorId); /** * 创建天卡订单(从 sign 鉴权上下文获取 deviceId) diff --git a/src/main/java/io/as/modules/daycard/service/DayCardPriceConfigService.java b/src/main/java/io/as/modules/daycard/service/DayCardPriceConfigService.java index eb5b320..3ce3ea2 100644 --- a/src/main/java/io/as/modules/daycard/service/DayCardPriceConfigService.java +++ b/src/main/java/io/as/modules/daycard/service/DayCardPriceConfigService.java @@ -14,8 +14,11 @@ public interface DayCardPriceConfigService extends IService params); + PageUtils queryPage(Map params, Long operatorId); /** * 获取有效定价策略(3 层维度,优先级从高到低) diff --git a/src/main/java/io/as/modules/daycard/service/DayCardRefundService.java b/src/main/java/io/as/modules/daycard/service/DayCardRefundService.java index d5b7bb3..81f607d 100644 --- a/src/main/java/io/as/modules/daycard/service/DayCardRefundService.java +++ b/src/main/java/io/as/modules/daycard/service/DayCardRefundService.java @@ -13,8 +13,11 @@ public interface DayCardRefundService extends IService { /** * 分页查询退款记录 + * + * @param params 查询参数 + * @param operatorId 当前操作人ID(用于组织数据隔离) */ - PageUtils queryPage(Map params); + PageUtils queryPage(Map params, Long operatorId); /** * 根据退款单号查询退款记录 diff --git a/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java b/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java index d46c51a..54298e1 100644 --- a/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java +++ b/src/main/java/io/as/modules/daycard/service/impl/DayCardOrderServiceImpl.java @@ -28,6 +28,7 @@ import io.as.modules.jst.service.JushitingApiService; import io.as.modules.jst.service.JstActivateCardResult; import io.as.modules.payment.service.WechatPayService; import io.as.modules.sys.entity.SysOrgEntity; +import io.as.modules.sys.service.SysOrgService; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,6 +86,9 @@ public class DayCardOrderServiceImpl extends ServiceImpl params, Long partnerId) { + public PageUtils queryPage(Map params, Long operatorId) { IPage page = new Query().getPage(params, "created_at", false); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL); - if (partnerId != null) { - wrapper.eq("partner_id", partnerId); - } String orderNo = (String) params.get("orderNo"); if (StringUtils.isNotBlank(orderNo)) { wrapper.eq("order_no", orderNo); @@ -108,6 +109,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl result = baseMapper.selectPage(page, wrapper); return new PageUtils(result); diff --git a/src/main/java/io/as/modules/daycard/service/impl/DayCardPriceConfigServiceImpl.java b/src/main/java/io/as/modules/daycard/service/impl/DayCardPriceConfigServiceImpl.java index 6cf2ece..58e2995 100644 --- a/src/main/java/io/as/modules/daycard/service/impl/DayCardPriceConfigServiceImpl.java +++ b/src/main/java/io/as/modules/daycard/service/impl/DayCardPriceConfigServiceImpl.java @@ -9,7 +9,9 @@ import io.as.modules.daycard.constants.DayCardConstants; import io.as.modules.daycard.dao.DayCardPriceConfigDao; import io.as.modules.daycard.entity.DayCardPriceConfigEntity; import io.as.modules.daycard.service.DayCardPriceConfigService; +import io.as.modules.sys.service.SysOrgService; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -21,8 +23,11 @@ import java.util.Map; @Service("dayCardPriceConfigService") public class DayCardPriceConfigServiceImpl extends ServiceImpl implements DayCardPriceConfigService { + @Autowired + private SysOrgService sysOrgService; + @Override - public PageUtils queryPage(Map params) { + public PageUtils queryPage(Map params, Long operatorId) { IPage page = new Query().getPage(params, "created_at", false); QueryWrapper wrapper = new QueryWrapper<>(); @@ -45,6 +50,8 @@ public class DayCardPriceConfigServiceImpl extends ServiceImpl result = baseMapper.selectPage(page, wrapper); return new PageUtils(result); } diff --git a/src/main/java/io/as/modules/daycard/service/impl/DayCardRefundServiceImpl.java b/src/main/java/io/as/modules/daycard/service/impl/DayCardRefundServiceImpl.java index 20823e0..14c8e30 100644 --- a/src/main/java/io/as/modules/daycard/service/impl/DayCardRefundServiceImpl.java +++ b/src/main/java/io/as/modules/daycard/service/impl/DayCardRefundServiceImpl.java @@ -18,6 +18,7 @@ import io.as.modules.daycard.entity.ProfitSharingDetailEntity; import io.as.modules.daycard.entity.ProfitSharingOrderEntity; import io.as.modules.daycard.service.DayCardRefundService; import io.as.modules.payment.service.WechatPayResult; +import io.as.modules.sys.service.SysOrgService; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +47,9 @@ public class DayCardRefundServiceImpl extends ServiceImpl params) { + public PageUtils queryPage(Map params, Long operatorId) { IPage page = new Query().getPage(params, "created_at", false); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL); + // 组织数据隔离:退款表无 store_org_id,通过 order_no 关联 day_card_order 做子查询过滤 + String mpath = sysOrgService.getUserOrgMpath(operatorId); + if (mpath != null) { + wrapper.apply("order_no IN (SELECT order_no FROM day_card_order WHERE delete_flag = 'UNDEL' " + + "AND store_org_id IN (SELECT id FROM sys_org WHERE delete_flag = 'UNDEL' AND mpath LIKE CONCAT({0}, '%')))", + mpath); + } + String refundNo = (String) params.get("refundNo"); if (StringUtils.isNotBlank(refundNo)) { wrapper.eq("refund_no", refundNo); diff --git a/src/main/java/io/as/modules/device/controller/DeviceController.java b/src/main/java/io/as/modules/device/controller/DeviceController.java index 4a10f0d..7bf0934 100644 --- a/src/main/java/io/as/modules/device/controller/DeviceController.java +++ b/src/main/java/io/as/modules/device/controller/DeviceController.java @@ -5,6 +5,8 @@ import io.as.common.utils.PageUtils; import io.as.common.utils.R; import io.as.modules.device.entity.DeviceEntity; import io.as.modules.device.service.DeviceService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -19,18 +21,21 @@ import java.util.Map; @Api(tags = "终端管理") @RestController @RequestMapping("/manage/device") -public class DeviceController { +public class DeviceController extends AbstractController { @Autowired private DeviceService deviceService; + @Autowired + private SysOrgService sysOrgService; + /** * 分页查询设备列表 */ @ApiOperation("分页查询设备列表") @GetMapping("/list") public R list(@RequestParam Map params) { - PageUtils page = deviceService.queryPage(params); + PageUtils page = deviceService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -43,6 +48,9 @@ public class DeviceController { @GetMapping("/info/{id}") public R info(@PathVariable("id") Long id) { DeviceEntity device = deviceService.getById(id); + if (device != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限查看该设备"); + } Map data = new HashMap<>(); data.put("device", device); return R.dataSuccess(data); @@ -55,8 +63,13 @@ public class DeviceController { @AuditAction(value = "DEVICE_SAVE", description = "新增设备") @PostMapping("/save") public R save(@RequestBody DeviceEntity device) { + if (device.getStoreOrgId() != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限操作该门店"); + } deviceService.saveDevice(device); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", device.getId()); + return R.dataSuccess(data); } /** @@ -66,8 +79,12 @@ public class DeviceController { @AuditAction(value = "DEVICE_UPDATE", description = "修改设备") @PostMapping("/update") public R update(@RequestBody DeviceEntity device) { + DeviceEntity existing = deviceService.getById(device.getId()); + if (existing != null && !sysOrgService.isOrgInScope(existing.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限操作该设备"); + } deviceService.updateDevice(device); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -77,8 +94,14 @@ public class DeviceController { @AuditAction(value = "DEVICE_DELETE", description = "删除设备") @PostMapping("/delete") public R delete(@RequestBody Long[] ids) { + for (Long id : ids) { + DeviceEntity device = deviceService.getById(id); + if (device != null && !sysOrgService.isOrgInScope(device.getStoreOrgId(), getUserId())) { + return R.error(403, "无权限删除该设备"); + } + } deviceService.delete(ids); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -88,7 +111,10 @@ public class DeviceController { @AuditAction(value = "DEVICE_BIND_STORE", description = "设备绑定门店") @PostMapping("/bindStore") public R bindStore(@RequestParam("deviceId") Long deviceId, @RequestParam("storeOrgId") Long storeOrgId) { + if (!sysOrgService.isOrgInScope(storeOrgId, getUserId())) { + return R.error(403, "无权限绑定到此门店"); + } deviceService.bindStore(deviceId, storeOrgId); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } } diff --git a/src/main/java/io/as/modules/device/service/DeviceService.java b/src/main/java/io/as/modules/device/service/DeviceService.java index 564e401..2a60e87 100644 --- a/src/main/java/io/as/modules/device/service/DeviceService.java +++ b/src/main/java/io/as/modules/device/service/DeviceService.java @@ -14,10 +14,11 @@ public interface DeviceService extends IService { /** * 分页查询设备列表 * - * @param params 查询参数(支持 storeOrgId/status/deviceName/mac 筛选) + * @param params 查询参数(支持 storeOrgId/status/deviceName/mac 筛选) + * @param operatorId 当前操作人userId(用于组织数据隔离) * @return 分页结果 */ - PageUtils queryPage(Map params); + PageUtils queryPage(Map params, Long operatorId); /** * 根据ID获取设备详情 diff --git a/src/main/java/io/as/modules/device/service/impl/DeviceServiceImpl.java b/src/main/java/io/as/modules/device/service/impl/DeviceServiceImpl.java index da0e6cf..1dcb8f7 100644 --- a/src/main/java/io/as/modules/device/service/impl/DeviceServiceImpl.java +++ b/src/main/java/io/as/modules/device/service/impl/DeviceServiceImpl.java @@ -9,7 +9,9 @@ import io.as.common.utils.Query; import io.as.modules.device.dao.DeviceDao; import io.as.modules.device.entity.DeviceEntity; import io.as.modules.device.service.DeviceService; +import io.as.modules.sys.service.SysOrgService; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @@ -21,23 +23,29 @@ import java.util.Map; @Service("deviceService") public class DeviceServiceImpl extends ServiceImpl implements DeviceService { + @Autowired + private SysOrgService sysOrgService; + @Override - public PageUtils queryPage(Map params) { + public PageUtils queryPage(Map params, Long operatorId) { String storeOrgId = (String) params.get("storeOrgId"); String status = (String) params.get("status"); String deviceName = (String) params.get("deviceName"); String mac = (String) params.get("mac"); String source = (String) params.get("source"); + QueryWrapper wrapper = new QueryWrapper() + .eq(StringUtils.isNotBlank(storeOrgId), "store_org_id", storeOrgId) + .eq(StringUtils.isNotBlank(status), "status", status) + .like(StringUtils.isNotBlank(deviceName), "device_name", deviceName) + .like(StringUtils.isNotBlank(mac), "mac", mac) + .eq(StringUtils.isNotBlank(source), "source", source) + .eq("delete_flag", "UNDEL"); + sysOrgService.applyBusinessOrgScope(wrapper, "store_org_id", operatorId); + IPage page = this.page( new Query().getPage(params, "created_at", false), - new QueryWrapper() - .eq(StringUtils.isNotBlank(storeOrgId), "store_org_id", storeOrgId) - .eq(StringUtils.isNotBlank(status), "status", status) - .like(StringUtils.isNotBlank(deviceName), "device_name", deviceName) - .like(StringUtils.isNotBlank(mac), "mac", mac) - .eq(StringUtils.isNotBlank(source), "source", source) - .eq("delete_flag", "UNDEL") + wrapper ); return new PageUtils(page); diff --git a/src/main/java/io/as/modules/openapi/controller/ContentPackageController.java b/src/main/java/io/as/modules/openapi/controller/ContentPackageController.java index 51eba6d..94077be 100644 --- a/src/main/java/io/as/modules/openapi/controller/ContentPackageController.java +++ b/src/main/java/io/as/modules/openapi/controller/ContentPackageController.java @@ -97,7 +97,7 @@ public class ContentPackageController { } contentPackage.setUpdatedAt(new Date()); contentPackageService.updateById(contentPackage); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -114,7 +114,7 @@ public class ContentPackageController { contentPackage.setUpdatedAt(new Date()); contentPackageService.updateById(contentPackage); } - return R.ok(); + return R.dataSuccess(new HashMap<>()); } // ==================== 内容选择辅助接口 ==================== diff --git a/src/main/java/io/as/modules/openapi/controller/OpenApiPartnerController.java b/src/main/java/io/as/modules/openapi/controller/OpenApiPartnerController.java index 1092df9..83699ed 100644 --- a/src/main/java/io/as/modules/openapi/controller/OpenApiPartnerController.java +++ b/src/main/java/io/as/modules/openapi/controller/OpenApiPartnerController.java @@ -90,7 +90,7 @@ public class OpenApiPartnerController { } partner.setUpdatedAt(new Date()); openApiPartnerService.updateById(partner); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -107,6 +107,6 @@ public class OpenApiPartnerController { partner.setUpdatedAt(new Date()); openApiPartnerService.updateById(partner); } - return R.ok(); + return R.dataSuccess(new HashMap<>()); } } diff --git a/src/main/java/io/as/modules/payment/controller/WechatPayConfigController.java b/src/main/java/io/as/modules/payment/controller/WechatPayConfigController.java index 7cc1345..f77a888 100644 --- a/src/main/java/io/as/modules/payment/controller/WechatPayConfigController.java +++ b/src/main/java/io/as/modules/payment/controller/WechatPayConfigController.java @@ -1,10 +1,13 @@ package io.as.modules.payment.controller; import io.as.common.annotation.AuditAction; +import io.as.common.exception.ASException; import io.as.common.utils.PageUtils; import io.as.common.utils.R; import io.as.modules.payment.entity.WechatPayConfigEntity; import io.as.modules.payment.service.WechatPayConfigService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; @@ -19,18 +22,21 @@ import java.util.Map; @Api(tags = "支付配置管理") @RestController @RequestMapping("/manage/payconfig") -public class WechatPayConfigController { +public class WechatPayConfigController extends AbstractController { @Autowired private WechatPayConfigService wechatPayConfigService; + @Autowired + private SysOrgService sysOrgService; + /** * 分页查询支付配置列表 */ @ApiOperation("分页查询支付配置列表") @GetMapping("/list") public R list(@RequestParam Map params) { - PageUtils page = wechatPayConfigService.queryPage(params); + PageUtils page = wechatPayConfigService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -43,6 +49,10 @@ public class WechatPayConfigController { @GetMapping("/info/{id}") public R info(@PathVariable("id") Long id) { WechatPayConfigEntity config = wechatPayConfigService.getById(id); + if (config == null) { + throw new ASException("支付配置不存在"); + } + checkPayConfigOrgScope(config); Map data = new HashMap<>(); data.put("config", config); return R.dataSuccess(data); @@ -55,8 +65,11 @@ public class WechatPayConfigController { @AuditAction(value = "PAYCONFIG_SAVE", description = "新增支付配置") @PostMapping("/save") public R save(@RequestBody WechatPayConfigEntity config) { + checkPayConfigOrgScope(config); wechatPayConfigService.saveConfig(config); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", config.getId()); + return R.dataSuccess(data); } /** @@ -66,8 +79,16 @@ public class WechatPayConfigController { @AuditAction(value = "PAYCONFIG_UPDATE", description = "修改支付配置") @PostMapping("/update") public R update(@RequestBody WechatPayConfigEntity config) { + if (config.getId() == null) { + throw new ASException("id不能为空"); + } + WechatPayConfigEntity existing = wechatPayConfigService.getById(config.getId()); + if (existing == null) { + throw new ASException("支付配置不存在"); + } + checkPayConfigOrgScope(existing); wechatPayConfigService.update(config); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -77,7 +98,28 @@ public class WechatPayConfigController { @AuditAction(value = "PAYCONFIG_DELETE", description = "删除支付配置") @PostMapping("/delete") public R delete(@RequestBody Long[] ids) { + for (Long id : ids) { + WechatPayConfigEntity config = wechatPayConfigService.getById(id); + if (config != null) { + checkPayConfigOrgScope(config); + } + } wechatPayConfigService.delete(ids); - return R.ok(); + return R.dataSuccess(new HashMap<>()); + } + + private void checkPayConfigOrgScope(WechatPayConfigEntity config) { + Long orgId = config.getStoreOrgId() != null ? config.getStoreOrgId() + : config.getAgentOrgId() != null ? config.getAgentOrgId() + : config.getChannelOrgId(); + if (orgId != null) { + if (!sysOrgService.isOrgInScope(orgId, getUserId())) { + throw new ASException("无权限操作该组织的数据"); + } + } else { + if (sysOrgService.getUserOrgMpath(getUserId()) != null) { + throw new ASException("无权限操作全局默认配置"); + } + } } } diff --git a/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java b/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java index ea06f19..acd4b1b 100644 --- a/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java +++ b/src/main/java/io/as/modules/payment/service/WechatPayConfigService.java @@ -14,10 +14,11 @@ public interface WechatPayConfigService extends IService /** * 分页查询支付配置列表 * - * @param params 查询参数 + * @param params 查询参数 + * @param operatorId 当前操作人ID(用于组织数据隔离) * @return 分页结果 */ - PageUtils queryPage(Map params); + PageUtils queryPage(Map params, Long operatorId); /** * 根据ID获取支付配置详情 diff --git a/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java b/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java index 38e56eb..b886bac 100644 --- a/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java +++ b/src/main/java/io/as/modules/payment/service/impl/WechatPayConfigServiceImpl.java @@ -9,7 +9,9 @@ import io.as.common.utils.Query; import io.as.modules.payment.dao.WechatPayConfigDao; import io.as.modules.payment.entity.WechatPayConfigEntity; import io.as.modules.payment.service.WechatPayConfigService; +import io.as.modules.sys.service.SysOrgService; import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @@ -21,23 +23,39 @@ import java.util.Map; @Service("wechatPayConfigService") public class WechatPayConfigServiceImpl extends ServiceImpl implements WechatPayConfigService { + @Autowired + private SysOrgService sysOrgService; + @Override - public PageUtils queryPage(Map params) { + public PageUtils queryPage(Map params, Long operatorId) { String partnerId = (String) params.get("partnerId"); String status = (String) params.get("status"); String channelOrgId = (String) params.get("channelOrgId"); String agentOrgId = (String) params.get("agentOrgId"); String storeOrgId = (String) params.get("storeOrgId"); + QueryWrapper wrapper = new QueryWrapper() + .eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId) + .eq(StringUtils.isNotBlank(channelOrgId), "channel_org_id", channelOrgId) + .eq(StringUtils.isNotBlank(agentOrgId), "agent_org_id", agentOrgId) + .eq(StringUtils.isNotBlank(storeOrgId), "store_org_id", storeOrgId) + .eq(StringUtils.isNotBlank(status), "status", status) + .eq("delete_flag", "UNDEL"); + + // 组织数据隔离:非超管仅可见自身范围内的配置(store_org_id 命中或 store_org_id 为空时 channel_org_id 命中) + String mpath = sysOrgService.getUserOrgMpath(operatorId); + if (mpath != null) { + wrapper.and(w -> w + .apply("store_org_id IN (SELECT id FROM sys_org WHERE delete_flag = 'UNDEL' AND mpath LIKE CONCAT({0}, '%'))", mpath) + .or() + .isNull("store_org_id") + .apply("channel_org_id IN (SELECT id FROM sys_org WHERE delete_flag = 'UNDEL' AND mpath LIKE CONCAT({0}, '%'))", mpath) + ); + } + IPage page = this.page( new Query().getPage(params, "created_at", false), - new QueryWrapper() - .eq(StringUtils.isNotBlank(partnerId), "partner_id", partnerId) - .eq(StringUtils.isNotBlank(channelOrgId), "channel_org_id", channelOrgId) - .eq(StringUtils.isNotBlank(agentOrgId), "agent_org_id", agentOrgId) - .eq(StringUtils.isNotBlank(storeOrgId), "store_org_id", storeOrgId) - .eq(StringUtils.isNotBlank(status), "status", status) - .eq("delete_flag", "UNDEL") + wrapper ); return new PageUtils(page); diff --git a/src/main/java/io/as/modules/stats/controller/StatsController.java b/src/main/java/io/as/modules/stats/controller/StatsController.java index 340e762..fb89ad7 100644 --- a/src/main/java/io/as/modules/stats/controller/StatsController.java +++ b/src/main/java/io/as/modules/stats/controller/StatsController.java @@ -2,6 +2,8 @@ package io.as.modules.stats.controller; import io.as.common.utils.R; import io.as.modules.stats.service.StatsService; +import io.as.modules.sys.controller.AbstractController; +import io.as.modules.sys.service.SysOrgService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -20,18 +22,21 @@ import java.util.Map; @Api(tags = "统计信息") @RestController @RequestMapping("/manage/stats") -public class StatsController { +public class StatsController extends AbstractController { @Autowired private StatsService statsService; + @Autowired + private SysOrgService sysOrgService; + /** * 总览看板 */ @ApiOperation("总览看板") @GetMapping("/overview") public R overview() { - Map data = statsService.getOverview(); + Map data = statsService.getOverview(getUserId()); Map result = new HashMap<>(); result.put("overview", data); return R.dataSuccess(result); @@ -47,7 +52,7 @@ public class StatsController { @RequestParam String startDate, @ApiParam(name = "endDate", value = "结束日期,格式 yyyy-MM-dd", required = true) @RequestParam String endDate) { - Map data = statsService.getOrderStats(startDate, endDate); + Map data = statsService.getOrderStats(startDate, endDate, getUserId()); Map result = new HashMap<>(); result.put("orderStats", data); return R.dataSuccess(result); @@ -59,7 +64,7 @@ public class StatsController { @ApiOperation("库存统计") @GetMapping("/pool") public R pool() { - Map data = statsService.getPoolStats(); + Map data = statsService.getPoolStats(getUserId()); Map result = new HashMap<>(); result.put("poolStats", data); return R.dataSuccess(result); @@ -71,7 +76,7 @@ public class StatsController { @ApiOperation("渠道统计") @GetMapping("/channels") public R channels() { - Map data = statsService.getChannelStats(); + Map data = statsService.getChannelStats(getUserId()); Map result = new HashMap<>(); result.put("channelStats", data); return R.dataSuccess(result); diff --git a/src/main/java/io/as/modules/stats/service/StatsService.java b/src/main/java/io/as/modules/stats/service/StatsService.java index 227e579..669a0f9 100644 --- a/src/main/java/io/as/modules/stats/service/StatsService.java +++ b/src/main/java/io/as/modules/stats/service/StatsService.java @@ -11,31 +11,35 @@ public interface StatsService { * 总览看板 * 返回:总订单数、今日订单数、总收入、今日收入、库存总量、可用库存、已使用卡数、激活失败卡数 * + * @param operatorId 当前操作人ID(用于组织数据隔离) * @return 统计数据 */ - Map getOverview(); + Map getOverview(Long operatorId); /** * 订单趋势统计(按日期分组的订单数和收入) * * @param startDate 开始日期,格式 yyyy-MM-dd * @param endDate 结束日期,格式 yyyy-MM-dd + * @param operatorId 当前操作人ID(用于组织数据隔离) * @return 按日期分组的订单趋势数据 */ - Map getOrderStats(String startDate, String endDate); + Map getOrderStats(String startDate, String endDate, Long operatorId); /** * 库存统计 * 返回:按状态分组(UNUSED/RESERVED/USED)数量,按激活状态分组(PENDING/SUCCESS/FAILED)数量 * + * @param operatorId 当前操作人ID(用于组织数据隔离) * @return 库存统计数据 */ - Map getPoolStats(); + Map getPoolStats(Long operatorId); /** * 渠道统计(按 channel_org_id 分组的订单数和收入) * + * @param operatorId 当前操作人ID(用于组织数据隔离) * @return 渠道统计数据 */ - Map getChannelStats(); + Map getChannelStats(Long operatorId); } diff --git a/src/main/java/io/as/modules/stats/service/impl/StatsServiceImpl.java b/src/main/java/io/as/modules/stats/service/impl/StatsServiceImpl.java index ab59387..c2813a1 100644 --- a/src/main/java/io/as/modules/stats/service/impl/StatsServiceImpl.java +++ b/src/main/java/io/as/modules/stats/service/impl/StatsServiceImpl.java @@ -1,6 +1,7 @@ package io.as.modules.stats.service.impl; import io.as.modules.stats.service.StatsService; +import io.as.modules.sys.service.SysOrgService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @@ -19,85 +20,183 @@ public class StatsServiceImpl implements StatsService { @Autowired private JdbcTemplate jdbcTemplate; + @Autowired + private SysOrgService sysOrgService; + + /** 组织数据隔离 SQL 片段:store_org_id 在操作人组织子树内 */ + private static final String ORG_SCOPE_FILTER = + " AND store_org_id IN (SELECT id FROM sys_org WHERE delete_flag='UNDEL' AND mpath LIKE CONCAT(?, '%'))"; + + /** 组织数据隔离 SQL 片段(pool 表无 store_org_id,通过 order_no 关联 order 表) */ + private static final String POOL_ORG_SCOPE_FILTER = + " AND (order_no IS NULL OR order_no IN (SELECT order_no FROM day_card_order WHERE delete_flag='UNDEL'" + + " AND store_org_id IN (SELECT id FROM sys_org WHERE delete_flag='UNDEL' AND mpath LIKE CONCAT(?, '%'))))"; + @Override - public Map getOverview() { + public Map getOverview(Long operatorId) { Map result = new HashMap<>(); + String mpath = sysOrgService.getUserOrgMpath(operatorId); + String orgFilter = mpath != null ? ORG_SCOPE_FILTER : ""; + String poolFilter = mpath != null ? POOL_ORG_SCOPE_FILTER : ""; + // 总订单数 - Integer totalOrders = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL'", - Integer.class); + Integer totalOrders; + if (mpath != null) { + totalOrders = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL'" + orgFilter, + Integer.class, mpath); + } else { + totalOrders = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL'", + Integer.class); + } result.put("totalOrders", totalOrders != null ? totalOrders : 0); // 今日订单数 - Integer todayOrders = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL' AND DATE(created_at)=CURDATE()", - Integer.class); + Integer todayOrders; + if (mpath != null) { + todayOrders = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL' AND DATE(created_at)=CURDATE()" + orgFilter, + Integer.class, mpath); + } else { + todayOrders = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_order WHERE delete_flag='UNDEL' AND DATE(created_at)=CURDATE()", + Integer.class); + } result.put("todayOrders", todayOrders != null ? todayOrders : 0); // 总收入 - BigDecimal totalRevenue = jdbcTemplate.queryForObject( - "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID'", - BigDecimal.class); + BigDecimal totalRevenue; + if (mpath != null) { + totalRevenue = jdbcTemplate.queryForObject( + "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID'" + orgFilter, + BigDecimal.class, mpath); + } else { + totalRevenue = jdbcTemplate.queryForObject( + "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID'", + BigDecimal.class); + } result.put("totalRevenue", totalRevenue != null ? totalRevenue : BigDecimal.ZERO); // 今日收入 - BigDecimal todayRevenue = jdbcTemplate.queryForObject( - "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID' AND DATE(pay_time)=CURDATE()", - BigDecimal.class); + BigDecimal todayRevenue; + if (mpath != null) { + todayRevenue = jdbcTemplate.queryForObject( + "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID' AND DATE(pay_time)=CURDATE()" + orgFilter, + BigDecimal.class, mpath); + } else { + todayRevenue = jdbcTemplate.queryForObject( + "SELECT COALESCE(SUM(pay_amount),0) FROM day_card_order WHERE delete_flag='UNDEL' AND status='PAID' AND DATE(pay_time)=CURDATE()", + BigDecimal.class); + } result.put("todayRevenue", todayRevenue != null ? todayRevenue : BigDecimal.ZERO); // 库存总量 - Integer totalPool = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL'", - Integer.class); + Integer totalPool; + if (mpath != null) { + totalPool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL'" + poolFilter, + Integer.class, mpath); + } else { + totalPool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL'", + Integer.class); + } result.put("totalPool", totalPool != null ? totalPool : 0); // 可用库存 - Integer availablePool = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='UNUSED'", - Integer.class); + Integer availablePool; + if (mpath != null) { + availablePool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='UNUSED'" + poolFilter, + Integer.class, mpath); + } else { + availablePool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='UNUSED'", + Integer.class); + } result.put("availablePool", availablePool != null ? availablePool : 0); // 已使用卡数 - Integer usedPool = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='USED'", - Integer.class); + Integer usedPool; + if (mpath != null) { + usedPool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='USED'" + poolFilter, + Integer.class, mpath); + } else { + usedPool = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND status='USED'", + Integer.class); + } result.put("usedPool", usedPool != null ? usedPool : 0); // 激活失败卡数 - Integer activateFailed = jdbcTemplate.queryForObject( - "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND activate_status='FAILED'", - Integer.class); + Integer activateFailed; + if (mpath != null) { + activateFailed = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND activate_status='FAILED'" + poolFilter, + Integer.class, mpath); + } else { + activateFailed = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM day_card_pool WHERE delete_flag='UNDEL' AND activate_status='FAILED'", + Integer.class); + } result.put("activateFailed", activateFailed != null ? activateFailed : 0); return result; } @Override - public Map getOrderStats(String startDate, String endDate) { + public Map getOrderStats(String startDate, String endDate, Long operatorId) { Map result = new HashMap<>(); - List> list = jdbcTemplate.queryForList( - "SELECT DATE(created_at) as date, COUNT(1) as orderCount, " + - "COALESCE(SUM(CASE WHEN status='PAID' THEN pay_amount ELSE 0 END),0) as revenue " + - "FROM day_card_order WHERE delete_flag='UNDEL' " + - "AND created_at BETWEEN ? AND ? " + - "GROUP BY DATE(created_at) ORDER BY date", - startDate + " 00:00:00", endDate + " 23:59:59"); + String mpath = sysOrgService.getUserOrgMpath(operatorId); + String orgFilter = mpath != null ? ORG_SCOPE_FILTER : ""; + + List> list; + if (mpath != null) { + list = jdbcTemplate.queryForList( + "SELECT DATE(created_at) as date, COUNT(1) as orderCount, " + + "COALESCE(SUM(CASE WHEN status='PAID' THEN pay_amount ELSE 0 END),0) as revenue " + + "FROM day_card_order WHERE delete_flag='UNDEL' " + + "AND created_at BETWEEN ? AND ? " + + orgFilter + + " GROUP BY DATE(created_at) ORDER BY date", + startDate + " 00:00:00", endDate + " 23:59:59", mpath); + } else { + list = jdbcTemplate.queryForList( + "SELECT DATE(created_at) as date, COUNT(1) as orderCount, " + + "COALESCE(SUM(CASE WHEN status='PAID' THEN pay_amount ELSE 0 END),0) as revenue " + + "FROM day_card_order WHERE delete_flag='UNDEL' " + + "AND created_at BETWEEN ? AND ? " + + "GROUP BY DATE(created_at) ORDER BY date", + startDate + " 00:00:00", endDate + " 23:59:59"); + } result.put("list", list); return result; } @Override - public Map getPoolStats() { + public Map getPoolStats(Long operatorId) { Map result = new HashMap<>(); + String mpath = sysOrgService.getUserOrgMpath(operatorId); + String poolFilter = mpath != null ? POOL_ORG_SCOPE_FILTER : ""; + // 按状态分组 - List> statusStats = jdbcTemplate.queryForList( - "SELECT status, COUNT(1) as count FROM day_card_pool " + - "WHERE delete_flag='UNDEL' GROUP BY status"); + List> statusStats; + if (mpath != null) { + statusStats = jdbcTemplate.queryForList( + "SELECT status, COUNT(1) as count FROM day_card_pool " + + "WHERE delete_flag='UNDEL'" + poolFilter + " GROUP BY status", + mpath); + } else { + statusStats = jdbcTemplate.queryForList( + "SELECT status, COUNT(1) as count FROM day_card_pool " + + "WHERE delete_flag='UNDEL' GROUP BY status"); + } Map statusMap = new HashMap<>(); for (Map row : statusStats) { statusMap.put(String.valueOf(row.get("status")), row.get("count")); @@ -105,9 +204,17 @@ public class StatsServiceImpl implements StatsService { result.put("statusStats", statusMap); // 按激活状态分组 - List> activateStats = jdbcTemplate.queryForList( - "SELECT activate_status as activateStatus, COUNT(1) as count FROM day_card_pool " + - "WHERE delete_flag='UNDEL' GROUP BY activate_status"); + List> activateStats; + if (mpath != null) { + activateStats = jdbcTemplate.queryForList( + "SELECT activate_status as activateStatus, COUNT(1) as count FROM day_card_pool " + + "WHERE delete_flag='UNDEL'" + poolFilter + " GROUP BY activate_status", + mpath); + } else { + activateStats = jdbcTemplate.queryForList( + "SELECT activate_status as activateStatus, COUNT(1) as count FROM day_card_pool " + + "WHERE delete_flag='UNDEL' GROUP BY activate_status"); + } Map activateMap = new HashMap<>(); for (Map row : activateStats) { activateMap.put(String.valueOf(row.get("activateStatus")), row.get("count")); @@ -118,14 +225,27 @@ public class StatsServiceImpl implements StatsService { } @Override - public Map getChannelStats() { + public Map getChannelStats(Long operatorId) { Map result = new HashMap<>(); - List> list = jdbcTemplate.queryForList( - "SELECT COALESCE(o.channel_org_id, 0) as channelOrgId, COUNT(1) as orderCount, " + - "COALESCE(SUM(o.pay_amount),0) as revenue " + - "FROM day_card_order o WHERE o.delete_flag='UNDEL' " + - "GROUP BY o.channel_org_id"); + String mpath = sysOrgService.getUserOrgMpath(operatorId); + String orgFilter = mpath != null ? ORG_SCOPE_FILTER : ""; + + List> list; + if (mpath != null) { + list = jdbcTemplate.queryForList( + "SELECT COALESCE(o.channel_org_id, 0) as channelOrgId, COUNT(1) as orderCount, " + + "COALESCE(SUM(o.pay_amount),0) as revenue " + + "FROM day_card_order o WHERE o.delete_flag='UNDEL'" + orgFilter + + " GROUP BY o.channel_org_id", + mpath); + } else { + list = jdbcTemplate.queryForList( + "SELECT COALESCE(o.channel_org_id, 0) as channelOrgId, COUNT(1) as orderCount, " + + "COALESCE(SUM(o.pay_amount),0) as revenue " + + "FROM day_card_order o WHERE o.delete_flag='UNDEL' " + + "GROUP BY o.channel_org_id"); + } result.put("list", list); return result; diff --git a/src/main/java/io/as/modules/sys/controller/SysMenuController.java b/src/main/java/io/as/modules/sys/controller/SysMenuController.java index f7dd645..1036fde 100644 --- a/src/main/java/io/as/modules/sys/controller/SysMenuController.java +++ b/src/main/java/io/as/modules/sys/controller/SysMenuController.java @@ -77,7 +77,9 @@ public class SysMenuController extends AbstractController { public R save(@RequestBody SysMenuEntity menu) { verifyForm(menu); sysMenuService.save(menu); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", menu.getId()); + return R.dataSuccess(data); } @SysLog("修改菜单") @@ -85,7 +87,7 @@ public class SysMenuController extends AbstractController { public R update(@RequestBody SysMenuEntity menu) { verifyForm(menu); sysMenuService.updateById(menu); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } @SysLog("删除菜单") @@ -99,7 +101,7 @@ public class SysMenuController extends AbstractController { return R.error("请先删除子菜单或按钮"); } sysMenuService.delete(menuId); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } private void verifyForm(SysMenuEntity menu) { diff --git a/src/main/java/io/as/modules/sys/controller/SysOrgController.java b/src/main/java/io/as/modules/sys/controller/SysOrgController.java index 65d7fc0..df8a855 100644 --- a/src/main/java/io/as/modules/sys/controller/SysOrgController.java +++ b/src/main/java/io/as/modules/sys/controller/SysOrgController.java @@ -257,7 +257,7 @@ public class SysOrgController extends AbstractController { public R update(@RequestBody SysOrgEntity org) { ValidatorUtils.validateEntity(org); sysOrgService.updateOrg(org, getUserId()); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } /** @@ -268,6 +268,6 @@ public class SysOrgController extends AbstractController { @PostMapping("/delete") public R delete(@RequestBody Long[] ids) { sysOrgService.deleteBatch(ids, getUserId()); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } } diff --git a/src/main/java/io/as/modules/sys/controller/SysRoleController.java b/src/main/java/io/as/modules/sys/controller/SysRoleController.java index c15751c..99be982 100644 --- a/src/main/java/io/as/modules/sys/controller/SysRoleController.java +++ b/src/main/java/io/as/modules/sys/controller/SysRoleController.java @@ -55,7 +55,9 @@ public class SysRoleController extends AbstractController { public R save(@RequestBody SysRoleEntity role) { ValidatorUtils.validateEntity(role); sysRoleService.saveRole(role, getUserId()); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", role.getId()); + return R.dataSuccess(data); } @SysLog("修改角色") @@ -64,7 +66,7 @@ public class SysRoleController extends AbstractController { public R update(@RequestBody SysRoleEntity role) { ValidatorUtils.validateEntity(role); sysRoleService.update(role, getUserId()); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } @SysLog("删除角色") @@ -72,6 +74,6 @@ public class SysRoleController extends AbstractController { @PostMapping("/delete") public R delete(@RequestBody Long[] roleIds) { sysRoleService.deleteBatch(roleIds); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } } diff --git a/src/main/java/io/as/modules/sys/controller/SysUserController.java b/src/main/java/io/as/modules/sys/controller/SysUserController.java index 0588a12..bd5ab75 100644 --- a/src/main/java/io/as/modules/sys/controller/SysUserController.java +++ b/src/main/java/io/as/modules/sys/controller/SysUserController.java @@ -2,6 +2,7 @@ package io.as.modules.sys.controller; import io.as.common.annotation.AuditAction; import io.as.common.annotation.SysLog; +import io.as.common.exception.ASException; import io.as.common.utils.Constant; import io.as.common.utils.PageUtils; import io.as.common.utils.R; @@ -13,6 +14,7 @@ import io.as.modules.sys.constants.OrgTypeRoleConstants; import io.as.modules.sys.entity.SysRoleEntity; import io.as.modules.sys.entity.SysUserEntity; import io.as.modules.sys.form.PasswordForm; +import io.as.modules.sys.service.SysOrgService; import io.as.modules.sys.service.SysRoleService; import io.as.modules.sys.service.SysUserRoleService; import io.as.modules.sys.service.SysUserService; @@ -34,12 +36,14 @@ public class SysUserController extends AbstractController { private SysUserRoleService sysUserRoleService; @Autowired private SysRoleService sysRoleService; + @Autowired + private SysOrgService sysOrgService; private static final String ROLE_NAME_PLATFORM = "PLATFORM"; @GetMapping("/list") public R list(@RequestParam Map params) { - PageUtils page = sysUserService.queryPage(params); + PageUtils page = sysUserService.queryPage(params, getUserId()); Map data = new HashMap<>(); data.put("page", page); return R.dataSuccess(data); @@ -51,7 +55,7 @@ public class SysUserController extends AbstractController { Map data = new HashMap<>(); data.put("user", u); data.put("roleName", queryRoleNameByUserId(getUserId())); - return R.ok().put("data", data); + return R.dataSuccess(data); } private String queryRoleNameByUserId(long userId) { @@ -83,12 +87,16 @@ public class SysUserController extends AbstractController { if (!flag) { return R.error("原密码不正确"); } - return R.ok(); + return R.dataSuccess(new HashMap<>()); } @GetMapping("/info/{userId}") public R info(@PathVariable("userId") Long userId) { SysUserEntity user = sysUserService.getById(userId); + if (user == null) { + throw new ASException("用户不存在"); + } + checkUserOrgScope(user); List roleIdList = sysUserRoleService.queryRoleIdList(userId); user.setRoleIdList(roleIdList); Map data = new HashMap<>(); @@ -102,7 +110,9 @@ public class SysUserController extends AbstractController { public R save(@RequestBody SysUserEntity user) { ValidatorUtils.validateEntity(user, AddGroup.class); sysUserService.saveUser(user, getUserId()); - return R.ok(); + Map data = new HashMap<>(); + data.put("id", user.getId()); + return R.dataSuccess(data); } @SysLog("修改用户") @@ -111,7 +121,7 @@ public class SysUserController extends AbstractController { public R update(@RequestBody SysUserEntity user) { ValidatorUtils.validateEntity(user, UpdateGroup.class); sysUserService.update(user, getUserId()); - return R.ok(); + return R.dataSuccess(new HashMap<>()); } @SysLog("删除用户") @@ -124,7 +134,25 @@ public class SysUserController extends AbstractController { if (ArrayUtils.contains(userIds, getUserId())) { return R.error("当前用户不能删除"); } + for (Long userId : userIds) { + SysUserEntity user = sysUserService.getById(userId); + if (user != null) { + checkUserOrgScope(user); + } + } sysUserService.deleteBatch(userIds); - return R.ok(); + return R.dataSuccess(new HashMap<>()); + } + + private void checkUserOrgScope(SysUserEntity user) { + if (user.getOrgId() != null) { + if (!sysOrgService.isOrgInScope(user.getOrgId(), getUserId())) { + throw new ASException("无权限操作该组织的数据"); + } + } else { + if (sysOrgService.getUserOrgMpath(getUserId()) != null) { + throw new ASException("无权限操作平台用户"); + } + } } } diff --git a/src/main/java/io/as/modules/sys/service/SysOrgService.java b/src/main/java/io/as/modules/sys/service/SysOrgService.java index 1d151cb..6955f96 100644 --- a/src/main/java/io/as/modules/sys/service/SysOrgService.java +++ b/src/main/java/io/as/modules/sys/service/SysOrgService.java @@ -1,5 +1,6 @@ package io.as.modules.sys.service; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.IService; import io.as.common.utils.PageUtils; import io.as.modules.sys.entity.SysOrgEntity; @@ -37,6 +38,21 @@ public interface SysOrgService extends IService { */ boolean isOrgInScope(Long orgId, Long operatorId); + /** + * 获取操作人的组织mpath(用于业务数据隔离) + * @return 超管返回null(不过滤),否则返回mpath字符串 + */ + String getUserOrgMpath(Long operatorId); + + /** + * 向业务表QueryWrapper注入组织范围过滤条件 + *

超管不过滤;非超管按 orgColumn 字段关联sys_org的mpath前缀匹配

+ * @param wrapper 业务表QueryWrapper + * @param orgColumn 业务表中的组织字段名(如 store_org_id、channel_org_id) + * @param operatorId 当前操作人userId + */ + void applyBusinessOrgScope(QueryWrapper wrapper, String orgColumn, Long operatorId); + /** * 指定组织及其子树范围内、未删除的全部门店组织 ID(type=STORE) */ diff --git a/src/main/java/io/as/modules/sys/service/SysUserService.java b/src/main/java/io/as/modules/sys/service/SysUserService.java index 1761880..9cd26a9 100644 --- a/src/main/java/io/as/modules/sys/service/SysUserService.java +++ b/src/main/java/io/as/modules/sys/service/SysUserService.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Map; public interface SysUserService extends IService { - PageUtils queryPage(Map params); + PageUtils queryPage(Map params, Long operatorId); List queryAllPerms(Long userId); List queryAllMenuId(Long userId); SysUserEntity queryByUserName(String username); diff --git a/src/main/java/io/as/modules/sys/service/impl/SysOrgServiceImpl.java b/src/main/java/io/as/modules/sys/service/impl/SysOrgServiceImpl.java index 132ac58..e321cfd 100644 --- a/src/main/java/io/as/modules/sys/service/impl/SysOrgServiceImpl.java +++ b/src/main/java/io/as/modules/sys/service/impl/SysOrgServiceImpl.java @@ -965,6 +965,33 @@ public class SysOrgServiceImpl extends ServiceImpl impl return target.getMpath().startsWith(userOrg.getMpath()); } + @Override + public String getUserOrgMpath(Long operatorId) { + if (operatorId == null) { + return null; + } + SysUserEntity user = sysUserService.getById(operatorId); + if (user == null || Constant.isSuperAdmin(user.getOrgId())) { + return null; + } + SysOrgEntity org = this.getById(user.getOrgId()); + if (org == null || StringUtils.isBlank(org.getMpath())) { + return null; + } + return org.getMpath(); + } + + @Override + public void applyBusinessOrgScope(QueryWrapper wrapper, String orgColumn, Long operatorId) { + String mpath = getUserOrgMpath(operatorId); + if (mpath == null) { + // 超管,不过滤 + return; + } + // 非超管:orgColumn 字段值对应的组织 mpath 以当前用户 mpath 为前缀 + wrapper.apply(orgColumn + " IN (SELECT id FROM sys_org WHERE delete_flag = 'UNDEL' AND mpath LIKE CONCAT({0}, '%'))", mpath); + } + @Override public List listDescendantStoreOrgIds(Long orgId) { if (orgId == null) { diff --git a/src/main/java/io/as/modules/sys/service/impl/SysUserServiceImpl.java b/src/main/java/io/as/modules/sys/service/impl/SysUserServiceImpl.java index f4a6086..efc5b0a 100644 --- a/src/main/java/io/as/modules/sys/service/impl/SysUserServiceImpl.java +++ b/src/main/java/io/as/modules/sys/service/impl/SysUserServiceImpl.java @@ -8,11 +8,13 @@ import io.as.common.utils.Constant; import io.as.common.utils.PageUtils; import io.as.common.utils.Query; import io.as.modules.sys.constants.OrgTypeRoleConstants; +import org.springframework.context.annotation.Lazy; import io.as.modules.sys.dao.SysOrgDao; import io.as.modules.sys.dao.SysUserDao; import io.as.modules.sys.entity.SysOrgEntity; import io.as.modules.sys.entity.SysRoleEntity; import io.as.modules.sys.entity.SysUserEntity; +import io.as.modules.sys.service.SysOrgService; import io.as.modules.sys.service.SysRoleService; import io.as.modules.sys.vo.SysUserListVo; import io.as.modules.sys.service.SysUserRoleService; @@ -35,14 +37,26 @@ public class SysUserServiceImpl extends ServiceImpl i private SysRoleService sysRoleService; @Autowired private SysOrgDao sysOrgDao; + @Autowired + @Lazy + private SysOrgService sysOrgService; @Override - public PageUtils queryPage(Map params) { + public PageUtils queryPage(Map params, Long operatorId) { String username = (String) params.get("username"); + QueryWrapper wrapper = new QueryWrapper() + .like(StringUtils.isNotBlank(username), "username", username); + + // 组织数据隔离:非超管仅可见自身组织范围内的用户,且不可见超管用户(org_id IS NULL) + String mpath = sysOrgService.getUserOrgMpath(operatorId); + if (mpath != null) { + wrapper.isNotNull("org_id"); + sysOrgService.applyBusinessOrgScope(wrapper, "org_id", operatorId); + } + IPage page = this.page( new Query().getPage(params), - new QueryWrapper() - .like(StringUtils.isNotBlank(username), "username", username) + wrapper ); List list = page.getRecords(); List voList = new ArrayList<>();