package app import ( "encoding/json" "errors" "fmt" "net/http" "strings" "time" "github.com/go-sql-driver/mysql" ) type adminCreateUserRequest struct { Phone string `json:"phone"` Password string `json:"password"` Nickname string `json:"nickname"` Gender int `json:"gender"` City string `json:"city"` Bio string `json:"bio"` } // Administrator provisioning uses the same credentials as public registration, // but never creates a login session or marks the phone/identity as verified. func (a *App) adminCreateUser(w http.ResponseWriter, r *http.Request) { var req adminCreateUserRequest if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "用户资料格式不正确") return } req.Phone = strings.TrimSpace(req.Phone) req.Nickname = strings.TrimSpace(req.Nickname) req.City = strings.TrimSpace(req.City) req.Bio = strings.TrimSpace(req.Bio) if !validPhone(req.Phone) { fail(w, http.StatusBadRequest, 20001, "请输入有效的中国大陆手机号") return } if !validUserPassword(req.Password) { fail(w, http.StatusBadRequest, 20001, passwordRule) return } if req.Nickname == "" || len([]rune(req.Nickname)) > 50 || req.Gender < 0 || req.Gender > 2 || len([]rune(req.City)) > 50 || len([]rune(req.Bio)) > 500 { fail(w, http.StatusBadRequest, 20001, "昵称须为 1–50 字,城市最多 50 字,简介最多 500 字,性别须为有效选项") return } passwordHash, err := hashPassword(req.Password) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建用户失败") return } phoneCipher, err := a.encryptPhone(req.Phone) if err != nil { fail(w, http.StatusInternalServerError, 50001, "加密手机号失败") return } tx, err := a.db.BeginTx(r.Context(), nil) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建用户失败") return } defer func() { _ = tx.Rollback() }() publicID := fmt.Sprintf("XY%d%s", time.Now().UnixMilli(), randomToken()[:5]) result, err := tx.ExecContext(r.Context(), `INSERT INTO users (public_id,country_code,phone_hash,phone_cipher,password_hash) VALUES (?,'+86',?,?,?)`, publicID, phoneHash(req.Phone), phoneCipher, passwordHash) if err != nil { var mysqlErr *mysql.MySQLError if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 && strings.Contains(mysqlErr.Message, "uk_users_phone_hash") { fail(w, http.StatusConflict, 20001, "该手机号已被使用,请勿重复创建") } else { fail(w, http.StatusInternalServerError, 50001, "创建用户失败,请稍后重试") } return } userID, err := result.LastInsertId() if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建用户失败") return } if _, err = tx.ExecContext(r.Context(), `INSERT INTO user_profiles (user_id,nickname,gender,city_name,bio,profile_score) VALUES (?,?,?,?,?,30)`, userID, req.Nickname, req.Gender, req.City, req.Bio); err != nil { fail(w, http.StatusInternalServerError, 50001, "创建用户资料失败") return } if _, err = tx.ExecContext(r.Context(), `INSERT INTO user_privacy_settings (user_id) VALUES (?)`, userID); err != nil { fail(w, http.StatusInternalServerError, 50001, "初始化隐私设置失败") return } if _, err = tx.ExecContext(r.Context(), `INSERT INTO user_notification_settings (user_id) VALUES (?)`, userID); err != nil { fail(w, http.StatusInternalServerError, 50001, "初始化通知设置失败") return } // Audit and account creation succeed together. Do not store the request body: // it contains the initial password and the full phone number. auditData, _ := json.Marshal(map[string]any{"publicId": publicID, "nickname": req.Nickname, "source": "admin"}) if _, err = tx.ExecContext(r.Context(), `INSERT INTO admin_audit_logs(admin_user_id,action,target_type,target_id,request_data,ip) VALUES (?,'create_user','user',?,?,?)`, current(r).ID, userID, auditData, clientIP(r)); err != nil { fail(w, http.StatusInternalServerError, 50001, "记录创建日志失败,用户未创建") return } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, 50001, "创建用户失败") return } reply(w, map[string]any{"id": userID, "publicId": publicID}) }