You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

231 lines
7.9 KiB

package console
import (
"errors"
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/mysql"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// ============================ 终端用户域 (user) ============================
//
// 与设备域(console 主库)不同:终端用户住在【所选应用的业务库】里,所以这里的每个接口
// 都要先从 X-App-Id 解出应用、拿到该应用的业务库连接,再查它的 user 表。
//
// 原来只有「按 UID 精确查一个人」,运营手上没有 UID 就什么也做不了。
// 现在补了列表(分页+条件筛选)与参数修改。
// userListPageMax 单页上限。列表带 COUNT(*),页太大在几十万用户的库上会很慢;
// 真要导出应该另开导出接口,而不是把 page_size 调到几千。
const userListPageMax = 200
// buildUserListWhere 把筛选条件拼成 where 子句。
//
// keyword 一栏多用:UID 精确、其余字段模糊。做成一栏是因为运营手上拿到的线索形态不定
// (客服转来的可能是手机号、可能是邮箱、也可能是用户自己报的昵称),
// 让人先选「按什么查」再输入只是多一步。
func buildUserListWhere(req *userListReq, now int64) (string, []interface{}) {
conds := make([]string, 0, 6)
args := make([]interface{}, 0, 8)
if kw := strings.TrimSpace(req.Keyword); kw != "" {
like := "%" + kw + "%"
conds = append(conds, "(uid = ? OR phone LIKE ? OR mail LIKE ? OR name LIKE ? OR account LIKE ? OR phonemac LIKE ?)")
args = append(args, kw, like, like, like, like, like)
}
if req.State >= 0 {
conds = append(conds, "`State` = ?")
args = append(args, req.State)
}
// vip: 1=会员中(未过期) 0=非会员。按到期时间判定而不是 viplv——
// viplv 是等级,过期后并不会被清零,拿它判会把过期用户也算成会员。
switch req.Vip {
case 1:
conds = append(conds, "vipexptime > ?")
args = append(args, now)
case 0:
conds = append(conds, "(vipexptime IS NULL OR vipexptime <= ?)")
args = append(args, now)
}
if req.Idverified >= 0 {
conds = append(conds, "idverified = ?")
args = append(args, req.Idverified == 1)
}
if req.Start > 0 {
conds = append(conds, "createtime >= ?")
args = append(args, req.Start)
}
if req.End > 0 {
conds = append(conds, "createtime < ?")
args = append(args, req.End)
}
if len(conds) == 0 {
return "1=1", args
}
return strings.Join(conds, " AND "), args
}
type userListReq struct {
Keyword string `json:"keyword"`
State int32 `json:"state"` // -1 不限;0 正常 1 禁用 2 已注销
Vip int32 `json:"vip"` // -1 不限;1 会员中 0 非会员
Idverified int32 `json:"idverified"` // -1 不限;1 已实名 0 未实名
Start int64 `json:"start"` // 注册时间区间 [start, end)
End int64 `json:"end"`
Page int `json:"page"` // 从 0 起
PageSize int `json:"page_size"` // 默认 20,上限 userListPageMax
}
// getUsers 终端用户列表:分页 + 条件筛选。
func (this *serverComp) getUsers(c *gin.Context) {
appId := parseAppId(c.GetHeader("X-App-Id"))
if !this.requireAppScope(c, appId) {
return
}
conn, err := this.module.registry.getServiceDB(appId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
// 三个「不限」标志位默认必须是 -1 而不是 0:0 是合法取值(正常状态 / 非会员 / 未实名),
// 拿零值当「不限」会让不传参的默认查询悄悄只返回一部分人。
req := &userListReq{State: -1, Vip: -1, Idverified: -1}
_ = c.ShouldBindJSON(req)
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > userListPageMax {
req.PageSize = userListPageMax
}
if req.Page < 0 {
req.Page = 0
}
where, args := buildUserListWhere(req, time.Now().Unix())
var total int64
if err := conn.Table(comm.TableUser).Where(where, args...).Count(&total).Error; err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
users := make([]*pb.DBUser, 0, req.PageSize)
if total > 0 {
if err := conn.Table(comm.TableUser).
Where(where, args...).
Order("createtime DESC").
Limit(req.PageSize).Offset(req.Page * req.PageSize).
Find(&users).Error; err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
}
for _, u := range users {
u.Password = "" // 列表同样不下发密码
}
writeOK(c, gin.H{"users": users, "total": total, "page": req.Page, "page_size": req.PageSize})
}
// updateUser 修改终端用户的可运营参数。
//
// ⚠️ 只放给超管/管理员。代理(Agent)不在此列——代理要发资源必须走
// api_giftuserresource,那条路会从代理自己的余额里扣;开放本接口等于让代理凭空造资源。
//
// ⚠️ 只改白名单里的几个字段,用 Updates(map) 局部更新而不是整行 Save:
// 整行写回会把并发中的登录时间、积分消耗等一起覆盖成后台读到的旧值。
func (this *serverComp) updateUser(c *gin.Context) {
if idt := currentIdentity(c); idt != pb.Identity_Admin && idt != pb.Identity_Manager {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可修改用户参数(代理请用「赠送资源点」)")
return
}
appId := parseAppId(c.GetHeader("X-App-Id"))
if !this.requireAppScope(c, appId) {
return
}
conn, err := this.module.registry.getServiceDB(appId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
// 指针字段:区分「没传这一项」与「传了 0」。用值类型会让「把积分清零」变成不可表达的操作。
var req struct {
Uid string `json:"uid"`
Name *string `json:"name"`
State *int32 `json:"state"`
Viplv *int32 `json:"viplv"`
Vipexptime *int64 `json:"vipexptime"`
Aichatintegral *int64 `json:"aichatintegral"`
Tradeintegral *int64 `json:"tradeintegral"`
Meetintegral *int64 `json:"meetintegral"`
}
_ = c.ShouldBindJSON(&req)
uid := strings.TrimSpace(req.Uid)
if uid == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "uid 必填")
return
}
old := &pb.DBUser{}
if err := conn.FindOne(comm.TableUser, old, "uid=?", uid); err != nil {
if errors.Is(err, mysql.ErrNoDocuments) {
writeErr(c, pb.ErrorCode_UserSessionNobeing, "未找到该用户")
return
}
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
updates := map[string]interface{}{}
if req.Name != nil {
updates["name"] = strings.TrimSpace(*req.Name)
}
if req.State != nil {
switch *req.State {
case int32(pb.UserState_Normal), int32(pb.UserState_Disabled), int32(pb.UserState_Cancelled):
updates["State"] = *req.State
default:
writeErr(c, pb.ErrorCode_ReqParameterError, "状态仅支持 0(正常) / 1(禁用) / 2(已注销)")
return
}
}
if req.Viplv != nil {
if *req.Viplv < 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "VIP 等级不能为负")
return
}
updates["viplv"] = *req.Viplv
}
if req.Vipexptime != nil {
if *req.Vipexptime < 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "VIP 到期时间不能为负")
return
}
updates["vipexptime"] = *req.Vipexptime
}
// 三种积分是「当前余额」,直接设成绝对值。总额(total*)不动——那是累计发放量,
// 属于流水统计口径,被后台改一次就再也对不上账了。
for k, v := range map[string]*int64{
"aichatintegral": req.Aichatintegral,
"tradeintegral": req.Tradeintegral,
"meetintegral": req.Meetintegral,
} {
if v == nil {
continue
}
if *v < 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "积分余额不能为负")
return
}
updates[k] = *v
}
if len(updates) == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "没有要修改的字段")
return
}
if err := conn.Table(comm.TableUser).Where("uid=?", uid).Updates(updates).Error; err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
writeOK(c, gin.H{"uid": uid, "updated": len(updates)})
}