越权
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s

This commit is contained in:
2026-07-28 16:57:27 +08:00
parent 1e4f998fe7
commit dc080436bf
7 changed files with 17 additions and 10 deletions

View File

@@ -66,6 +66,7 @@
| 企业微信审批加密回调与详情终态同步 | 回调为外部自动事实,不伪造人工 Audit Event验签、AES 解密、receiveid 校验失败只记录安全错误,不进入业务终态 | `tb_integration_log` 按应用和签名幂等保存入站密文哈希,`tb_wecom_approval_context.latest_detail_snapshot` 保存权威详情,通用审批实例及决策投递表保存标准终态 | 入站回调先写 Integration Log pending详情任务完成后置 completed每次 `getapprovaldetail` 写独立出站 Integration Log不保存 access_token但保存 sp_no、sp_status 和耗时摘要 | 回调只入队结构化 `wecom:approval:sync` 任务;权威终态通过现有 `SyncDecisionService` 原子写 `approval.terminal_decision.recorded`,不直接修改退款或钱包 |
| 企业微信审批主动恢复、未终态轮询与审批人读取投影 | N/A定时恢复和只读投影均为系统自动动作不伪造人工操作者映射不到账号不改变业务事实 | `tb_wecom_approval_context.submission_attempted_at/last_recovery_at/sp_no/latest_detail_snapshot` 与通用审批实例是恢复和展示的权威本地事实;只有唯一候选可从结果未知转为审批中 | 每次 `getapprovalinfo` 分页和 `getapprovaldetail` 均写独立出站 Integration Log只保存应用、时间窗、模板、发起人、单号数量、sp_no、状态和耗时等安全摘要不保存 Secret、access_token、media_id、附件正文或完整响应 | Scheduler 仅提交 `wecom:approval:recovery`;恢复和轮询仅提交结构化 `wecom:approval:sync`,不写审批提交 Outbox、不调用 `applyevent`,标准终态仍沿用既有终态 Outbox |
| 员工线下代充值申请与企微终态入账 | 申请保存以真实提交人及明文业务快照留痕;资金成功 Audit Event 延期至既有统一钱包治理任务,企微自动终态不伪造人工审批人 | `tb_agent_recharge_record``tb_approval_instance``tb_wecom_approval_context` 同事务保存申请事实approved 通过 `topup + recharge_record_id` 唯一成功钱包流水幂等入账,其他终态不修改钱包,通过后撤销不自动冲正;`tb_notification` 保存到账通知 | 申请创建本身不外呼后续附件上传、applyevent、详情与恢复沿用企微 Integration Log业务参数按用户确认保存明文日志仍不记录 Secret、access_token、media_id 或附件正文 | 创建事务写 `approval.submission.requested`;标准终态写 `approval.terminal_decision.recorded`approved 入账事务再写 `wallet.agent_main.credited` 和目标店铺的 `notification.admin.dynamic.requested`;在线充值复用同一入账接缝;新审批单禁止旧人工确认或驳回入口绕过 |
| 代理充值列表、详情与支付状态读取 | N/A普通受权读取请求进入 Access Log不改变充值、支付或钱包事实 | 只读 `tb_agent_recharge_record`、相关支付单和店铺名称;平台账号不限制,代理账号统一按当前上下文的自身及下级店铺 ID 过滤 | N/A不调用支付渠道或企微 | N/A纯 Query不产生 Outbox |
| 退款申请与企微终态处理 | 申请以真实提交人和订单/金额/凭证明文快照留痕;企微自动终态不伪造本地人工审批人,资金与佣金 Audit Event 继续沿用既有治理延期口径 | `tb_refund_request`、通用审批实例和企微上下文同事务保存approved 条件更新订单与退款单,代理主钱包按 refund ID、资产钱包按退款单号复核成功回款佣金按记录锁定并失效套餐按订单及换货迁移关系幂等失效`tb_notification` 保存退款完成通知 | 申请创建不外呼附件、applyevent、详情、回调和恢复沿用企微 Integration Log业务参数明文保存在业务/审批快照中但不复制到 Integration LogSecret、access_token、media_id 和附件正文仍禁止记录 | 创建事务写 `approval.submission.requested`;终态写 `approval.terminal_decision.recorded`;退款事务幂等写目标店铺的 `notification.admin.dynamic.requested`;业务消费者只有在订单、钱包、佣金和资产后处理完成后才确认投递成功,失败释放租约重试 |
| 退款与线下代充值旧审批入口发布切换 | N/A部署环境开关控制旧入口是否可用不新增业务操作实际旧入口操作继续沿用各自既有审计口径 | `approval_instance_id IS NULL` 是存量旧 provider 的兼容边界,非空记录只接受企微标准终态;关闭开关不修改或删除任何业务事实 | N/A开关判断不调用外部系统也不得写 Integration Log 冒充发布审计) | N/A开关判断不产生可靠副作用企微 Worker 继续消费既有标准终态 Outbox |
| 单列 CSV 资产套餐批量订购 | 任务创建和逐行订单继续进入 Access Log、任务操作者快照与既有订单/钱包审计接缝;本 Change 不新建批量任务专用 Audit Writer | `tb_asset_package_batch_order_task` 保存输入参数和逐行结果,成功行以 `tb_order`、订单明细、套餐使用、支付记录及代理钱包成功流水为权威业务事实 | 对象存储上传和下载沿用现有存储日志,不把文件正文写入 Integration Log本切片不新增外部支付或 Gateway 调用 | 创建接口提交结构化 `asset:package:batch_order` Asynq 任务;逐行钱包订单继续沿用既有钱包扣款 Outbox 和佣金任务,重复任务由状态条件与订单幂等规则阻断 |

View File

@@ -5,7 +5,7 @@
## 一、业务需求
- [ ] **UR#33 套餐临期提醒**:核对 015 天临期列表、03 天置顶、Dashboard 数量、C 端续费入口,以及 15/7/3 天向全部有效店铺账号、有效绑定的平台业务员和资产绑定个人账号发送防重站内通知;正文明确资产标识、到期状态和到期日期。
- [ ] **UR#34 充值审核流程**:核对代理在线扫码充值与平台线下代充值隔离;支付、企微审批、重复回调和入账补偿正确;到账后通知全部有效店铺账号及有效绑定的平台业务员,正文明确店铺名称和精确到分的充值金额。
- [ ] **UR#34 充值审核流程**:核对代理在线扫码充值与平台线下代充值隔离;支付、企微审批、重复回调和入账补偿正确;代理充值列表、详情和支付状态只能读取自己及下级店铺数据;到账后通知全部有效店铺账号及有效绑定的平台业务员,正文明确店铺名称和精确到分的充值金额。
- [ ] **UR#35 退款审核**:核对企微审批、整单终结、钱包回充、套餐失效和佣金处理;重复终态不重复退款或通知;退款完成后通知全部有效店铺账号及有效绑定的平台业务员。
- [ ] **UR#36 批量订购套餐**:核对 CSV 上传、跨代理资产、统一支付方式、逐行结果、部分成功、失败原因、任务恢复及重复执行不重复扣款。
- [ ] **UR#37 企业微信审核流转**:核对企微应用、模板、成员绑定、代理固定代提交人、回调、主动同步和异常恢复;系统内不得提供本地同意或拒绝入口。

View File

@@ -12366,6 +12366,7 @@ paths:
- 企业微信审批
/api/admin/agent-recharges:
get:
description: 平台账号可查看全部;代理账号仅返回自己店铺及下级店铺的充值订单。
parameters:
- description: 页码默认1
in: query

View File

@@ -10,6 +10,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// PaymentStatusQuery 查询代理充值本地支付与到账事实。
@@ -28,7 +29,8 @@ func (q *PaymentStatusQuery) Get(ctx context.Context, rechargeID uint) (*dto.Age
return nil, errors.New(errors.CodeInvalidParam, "代理充值支付状态查询参数无效")
}
var recharge model.AgentRechargeRecord
if err := q.db.WithContext(ctx).Where("id = ?", rechargeID).First(&recharge).Error; err != nil {
rechargeQuery := middleware.ApplyShopFilter(ctx, q.db.WithContext(ctx).Model(&model.AgentRechargeRecord{}))
if err := rechargeQuery.Where("id = ?", rechargeID).First(&recharge).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}

View File

@@ -30,11 +30,12 @@ func registerAgentRechargeRoutes(router fiber.Router, handler *admin.AgentRechar
})
Register(group, doc, groupPath, "GET", "", handler.List, RouteSpec{
Summary: "查询代理充值订单列表",
Tags: []string{"代理预充值"},
Input: new(dto.AgentRechargeListRequest),
Output: new(dto.AgentRechargeListResponse),
Auth: true,
Summary: "查询代理充值订单列表",
Description: "平台账号可查看全部;代理账号仅返回自己店铺及下级店铺的充值订单。",
Tags: []string{"代理预充值"},
Input: new(dto.AgentRechargeListRequest),
Output: new(dto.AgentRechargeListResponse),
Auth: true,
})
Register(group, doc, groupPath, "GET", "/payment-methods", handler.PaymentMethods, RouteSpec{

View File

@@ -411,7 +411,7 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.AgentRechargeRespo
record, err := s.agentRechargeStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "充值记录不存在")
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
}
@@ -444,7 +444,7 @@ func (s *Service) List(ctx context.Context, req *dto.AgentRechargeListRequest) (
pageSize = constants.DefaultPageSize
}
query := s.db.WithContext(ctx).Model(&model.AgentRechargeRecord{})
query := middleware.ApplyShopFilter(ctx, s.db.WithContext(ctx).Model(&model.AgentRechargeRecord{}))
if req.ShopID != nil {
query = query.Where("shop_id = ?", *req.ShopID)

View File

@@ -5,6 +5,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -48,7 +49,8 @@ func (s *AgentRechargeStore) GetByRechargeNo(ctx context.Context, rechargeNo str
// GetByID 根据 ID 查询
func (s *AgentRechargeStore) GetByID(ctx context.Context, id uint) (*model.AgentRechargeRecord, error) {
var record model.AgentRechargeRecord
if err := s.db.WithContext(ctx).First(&record, id).Error; err != nil {
query := middleware.ApplyShopFilter(ctx, s.db.WithContext(ctx).Model(&model.AgentRechargeRecord{}))
if err := query.First(&record, id).Error; err != nil {
return nil, err
}
return &record, nil