Files
junhong_cmp_fiber/internal/model/dto/shop_dto.go
break 575d056f54
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(代理分销提现): 落地扫码注册、提现资料资格与企微终审提现
AUG26-008。

- 迁移 000214–000217:tb_shop 全局唯一且不可修改的随机分销码(含存量回填)、
  tb_agent_distribution_registration 待审批注册记录、tb_withdrawal_qualification 资料版本、
  tb_commission_withdrawal_request_attempt 审批尝试记录,以及提现申请的 latest_*/异常标记列;
  不修改既有迁移,down 在存在本 Change 业务事实或新类型场景行时拒绝破坏性回滚。
- 公开接口 POST /api/c/v1/agent-distribution-registrations:无认证,复用既有短信验证码校验、
  消费与限流;无效分销码、停用上级、验证码无效或已消费统一返回「分销码不可用」且不落库,
  审批通过前不创建店铺、账号或钱包。
- 审批通过才在同一事务内建启用店铺、代理主账号、钱包、上级层级与业务员快照,驳回不建实体,
  重复回调不重复建实体,提交后清理上级下级缓存。
- 提现资料资格按不可变版本保存,替换合同或法人身份证即新增版本并同事务失效旧有效版本;
  超管作废原因必填;代理停用与店铺删除联动失效。
- 提现每次提交或重提新增不可变审批尝试记录并冻结金额;企业微信通过仅一次从冻结扣减、
  保持状态 2 并写 paid_at(不使用状态 4),驳回/cancelled/deleted 仅一次释放,
  通过后撤销不回滚、不重新冻结、只写正交异常标记;加锁顺序统一为申请→尝试→钱包。
- 本地人工终审对已关联审批实例的申请返回状态冲突,approval_instance_id 为空的存量申请保持既有行为,
  不新增任何配置开关。
- 补齐审批业务类型注册点全集:业务类型与场景字段常量、场景 DTO 两处枚举与中文描述、
  场景字段白名单/合法类型/中文名、数据库 CHECK、Worker 决策消费者与装配、审批审计资源映射,
  以及三个新审计资源与 13 个审计动作;失败/拒绝审计改为必达。
- 新增后台路由与 OpenAPI:资格提交/查询/作废、提现申请/重提/详情、店铺详情返回只读分销码。
- 归档本 Change:主 Spec 新增 agent-distribution-withdrawal 能力(5 个 Requirement)。

验证(junhong_cmp_test + Redis DB 6,显式 DB_*,未重置整库):
- 迁移 up → version 217 且 dirty=false → down 3 → up 回 217,fixture 复核残留为 0。
- 受控状态机脚手架 227 项通过 / 0 项失败,覆盖 18 组场景(幂等与乱序回调、资金冻结/释放/重提、
  退款回扣 × 在途提现并发、负向场景拒绝审计与 14 个动作码审计真实落库)。
- gofmt 空、go build/go vet 通过、gendocs 与工作区逐字节一致、context-health 通过、
  openspec validate --strict 通过、doctor healthy;自动化测试按项目决策为 N/A。

运行期前置(未完成,非代码交付物):由超管经 PUT /api/admin/wecom/scenes/{business_type} 为
agent_distribution_approval、withdrawal_qualification_approval、commission_withdrawal_approval
配置启用场景与模板控件映射;未配置时相应提交失败关闭。
2026-09-14 09:45:13 +08:00

188 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dto
import "github.com/bytedance/sonic"
type ShopListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
ShopName string `json:"shop_name" query:"shop_name" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称模糊查询"`
ShopCode string `json:"shop_code" query:"shop_code" validate:"omitempty,max=50" maxLength:"50" description:"店铺编号精确查询"`
ContactPhone string `json:"contact_phone" query:"contact_phone" validate:"omitempty,len=11,numeric,ascii" minLength:"11" maxLength:"11" pattern:"^[0-9]{11}$" description:"联系电话精确查询11位 ASCII 数字;空值不启用筛选;与其他条件按 AND 组合)"`
BusinessOwnerAccountID *uint `json:"business_owner_account_id" query:"business_owner_account_id" validate:"omitempty,min=1" minimum:"1" description:"平台业务员账号ID精确筛选"`
ParentID *uint `json:"parent_id" query:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID"`
Level *int `json:"level" query:"level" validate:"omitempty,min=1,max=7" minimum:"1" maximum:"7" description:"店铺层级 (1-7级)"`
Status *int `json:"status" query:"status" validate:"omitempty,oneof=0 1" description:"状态 (0:禁用, 1:启用)"`
}
type CreateShopRequest struct {
ShopName string `json:"shop_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"店铺名称"`
ShopCode string `json:"shop_code" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"店铺编号"`
ParentID *uint `json:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID一级店铺可不填"`
BusinessOwnerAccountID *uint `json:"business_owner_account_id" nullable:"true" description:"平台业务员账号ID缺失时继承上级null 表示空归属"`
BusinessOwnerAccountIDSet bool `json:"-"`
ContactName string `json:"contact_name" validate:"omitempty,max=50" maxLength:"50" description:"联系人姓名"`
ContactPhone string `json:"contact_phone" validate:"omitempty,len=11" minLength:"11" maxLength:"11" description:"联系人电话"`
Province string `json:"province" validate:"omitempty,max=50" maxLength:"50" description:"省份"`
City string `json:"city" validate:"omitempty,max=50" maxLength:"50" description:"城市"`
District string `json:"district" validate:"omitempty,max=50" maxLength:"50" description:"区县"`
Address string `json:"address" validate:"omitempty,max=255" maxLength:"255" description:"详细地址"`
DefaultRoleID uint `json:"default_role_id" validate:"required,min=1" required:"true" minimum:"1" description:"店铺默认角色ID必须是客户角色"`
InitPassword string `json:"init_password" validate:"required,min=8,max=32" required:"true" minLength:"8" maxLength:"32" description:"初始账号密码"`
InitUsername string `json:"init_username" validate:"required,min=3,max=50" required:"true" minLength:"3" maxLength:"50" description:"初始账号用户名"`
InitPhone string `json:"init_phone" validate:"required,len=11" required:"true" minLength:"11" maxLength:"11" description:"初始账号手机号"`
}
// UnmarshalJSON 解析创建店铺请求并保留业务员字段“缺失”和“显式 null”的差异。
func (r *CreateShopRequest) UnmarshalJSON(data []byte) error {
type plain CreateShopRequest
var decoded plain
if err := sonic.Unmarshal(data, &decoded); err != nil {
return err
}
var fields map[string]any
if err := sonic.Unmarshal(data, &fields); err != nil {
return err
}
decoded.BusinessOwnerAccountIDSet = false
if _, exists := fields["business_owner_account_id"]; exists {
decoded.BusinessOwnerAccountIDSet = true
}
*r = CreateShopRequest(decoded)
return nil
}
type UpdateShopRequest struct {
ShopName string `json:"shop_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"店铺名称"`
BusinessOwnerAccountID *uint `json:"business_owner_account_id" nullable:"true" description:"平台业务员账号ID缺失时保持不变null 表示清空"`
BusinessOwnerAccountIDSet bool `json:"-"`
ContactName string `json:"contact_name" validate:"omitempty,max=50" maxLength:"50" description:"联系人姓名"`
ContactPhone string `json:"contact_phone" validate:"omitempty,len=11" minLength:"11" maxLength:"11" description:"联系人电话"`
Province string `json:"province" validate:"omitempty,max=50" maxLength:"50" description:"省份"`
City string `json:"city" validate:"omitempty,max=50" maxLength:"50" description:"城市"`
District string `json:"district" validate:"omitempty,max=50" maxLength:"50" description:"区县"`
Address string `json:"address" validate:"omitempty,max=255" maxLength:"255" description:"详细地址"`
Status int `json:"status" validate:"oneof=0 1" required:"true" description:"状态 (0:禁用, 1:启用)"`
ClientLoginDisabled *bool `json:"client_login_disabled" description:"是否禁止该店铺资产发起新的 C 端登录;不传保持不变"`
}
// UnmarshalJSON 解析更新店铺请求并保留业务员字段“缺失”和“显式 null”的差异。
func (r *UpdateShopRequest) UnmarshalJSON(data []byte) error {
type plain UpdateShopRequest
var decoded plain
if err := sonic.Unmarshal(data, &decoded); err != nil {
return err
}
var fields map[string]any
if err := sonic.Unmarshal(data, &fields); err != nil {
return err
}
decoded.BusinessOwnerAccountIDSet = false
if _, exists := fields["business_owner_account_id"]; exists {
decoded.BusinessOwnerAccountIDSet = true
}
*r = UpdateShopRequest(decoded)
return nil
}
// ShopResponse 店铺响应
type ShopResponse struct {
ID uint `json:"id" description:"店铺ID"`
ShopName string `json:"shop_name" description:"店铺名称"`
ShopCode string `json:"shop_code" description:"店铺编号"`
// DistributionCode 是创建时生成的全局唯一随机分销码,创建后不可修改。
// 该字段为只读;它是该店铺作为上级被扫码注册的唯一入口标识,二维码只编码该码。
DistributionCode string `json:"distribution_code" description:"分销码(全局唯一、创建后不可修改的只读随机码;该店铺作为上级被扫码注册的唯一入口标识)"`
ParentID *uint `json:"parent_id,omitempty" description:"上级店铺ID"`
ParentShopName string `json:"parent_shop_name,omitempty" description:"上级店铺名称"`
BusinessOwnerAccountID *uint `json:"business_owner_account_id" description:"平台业务员账号IDnull 表示未归属"`
BusinessOwnerUsername string `json:"business_owner_username" description:"平台业务员账号名"`
BusinessOwnerPhoneSummary string `json:"business_owner_phone_summary" description:"平台业务员手机号摘要(前三后四)"`
BusinessOwnerAvailable bool `json:"business_owner_available" description:"平台业务员当前是否可用于通知接收"`
Level int `json:"level" description:"店铺层级 (1-7级)"`
ContactName string `json:"contact_name" description:"联系人姓名"`
ContactPhone string `json:"contact_phone" description:"联系人电话"`
Province string `json:"province" description:"省份"`
City string `json:"city" description:"城市"`
District string `json:"district" description:"区县"`
Address string `json:"address" description:"详细地址"`
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
ClientLoginDisabled bool `json:"client_login_disabled" description:"是否禁止该店铺资产发起新的 C 端登录"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
// ShopPageResult 店铺分页响应
type ShopPageResult struct {
Items []ShopResponse `json:"items" description:"店铺列表"`
Total int64 `json:"total" description:"总记录数"`
Page int `json:"page" description:"当前页码"`
Size int `json:"size" description:"每页数量"`
}
// UpdateShopParams 更新店铺聚合参数 (用于文档生成)
type UpdateShopParams struct {
IDReq
UpdateShopRequest
}
// ShopCascadeRequest 店铺联级查询请求
type ShopCascadeRequest struct {
ShopName string `json:"shop_name" query:"shop_name" validate:"omitempty,max=100" maxLength:"100" description:"店铺名称(模糊查询)"`
ParentID *uint `json:"parent_id" query:"parent_id" validate:"omitempty,min=1" minimum:"1" description:"上级店铺ID不传则查询顶级店铺"`
ExcludeSelf bool `json:"exclude_self" query:"exclude_self" description:"是否排除当前账号自身店铺默认false"`
}
// ShopCascadeItem 联级查询结果项
type ShopCascadeItem struct {
ID uint `json:"id" description:"店铺ID"`
ShopName string `json:"shop_name" description:"店铺名称"`
HasChildren bool `json:"has_children" description:"是否有下级店铺"`
}
// ShopBusinessOwnerCandidateRequest 是平台业务员候选分页查询。
type ShopBusinessOwnerCandidateRequest struct {
Keyword string `json:"keyword" query:"keyword" validate:"omitempty,max=50" maxLength:"50" description:"用户名或手机号关键词"`
Page int `json:"page" query:"page" validate:"omitempty,min=1,max=10000" minimum:"1" maximum:"10000" description:"页码,默认 1"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量,默认 20最大 100"`
}
// ShopBusinessOwnerCandidate 是平台业务员候选最小投影。
type ShopBusinessOwnerCandidate struct {
ID uint `json:"id" description:"平台业务员账号ID"`
Username string `json:"username" description:"平台业务员账号名"`
PhoneSummary string `json:"phone_summary" description:"手机号摘要(前三后四)"`
}
// ShopBusinessOwnerCandidatePageResult 是平台业务员候选分页结果。
type ShopBusinessOwnerCandidatePageResult struct {
Items []ShopBusinessOwnerCandidate `json:"items" description:"候选账号列表"`
Total int64 `json:"total" description:"总数量"`
Page int `json:"page" description:"当前页码"`
Size int `json:"size" description:"每页数量"`
}
// UpdateShopCreditLimitRequest 调整既有店铺实际信用额度请求。
type UpdateShopCreditLimitRequest struct {
CreditEnabled *bool `json:"credit_enabled" validate:"required" required:"true" description:"是否启用实际信用额度"`
CreditLimit *int64 `json:"credit_limit" validate:"required,min=0" required:"true" minimum:"0" description:"实际信用额度关闭时必须为0"`
}
// UpdateShopCreditLimitParams 调整既有店铺实际信用额度参数。
type UpdateShopCreditLimitParams struct {
IDReq
UpdateShopCreditLimitRequest
}
// ShopCreditLimitResponse 店铺实际信用额度调整响应。
type ShopCreditLimitResponse struct {
ShopID uint `json:"shop_id" description:"店铺ID"`
WalletID uint `json:"wallet_id" description:"代理主钱包ID"`
Balance int64 `json:"balance" description:"账面余额(分)"`
FrozenBalance int64 `json:"frozen_balance" description:"冻结金额(分)"`
CreditEnabled bool `json:"credit_enabled" description:"是否启用实际信用额度"`
CreditLimit int64 `json:"credit_limit" description:"实际信用额度(分)"`
AvailableBalance int64 `json:"available_balance" description:"总可用金额(分)"`
Version int `json:"version" description:"更新后的主钱包版本"`
}