优化uniapp
This commit is contained in:
1 parent
4f1acaa451
commit
8fc52c574f
49 files changed
+1952
-197
No files matched your search
@@ -97,6 +97,16 @@ func (c *AppAggregateController) Home() {
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
// 注册即登录:新用户尚未加入企业时令牌 tid=0。
|
||||
// 此时按「原免登录功能」返回公开模块与公开菜单,其余模块由前端引导创建/挂靠企业。
|
||||
if tid == 0 {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": c.noTenantHome(claims),
|
||||
})
|
||||
return
|
||||
}
|
||||
uid := services.ResolveTenantUID(tid, uint64(claims.UserID))
|
||||
|
||||
modules := c.appModules(tid)
|
||||
@@ -125,6 +135,77 @@ func (c *AppAggregateController) Home() {
|
||||
})
|
||||
}
|
||||
|
||||
// noTenantHome tid=0(已注册、尚未加入企业)时的首屏数据。
|
||||
// 模块沿用公开宫格(保留 require_login 作为「需企业」标记),菜单只下发免登录菜单,
|
||||
// 因此新注册用户能直接使用原「免登录」功能,进入需企业模块时由端上引导申请。
|
||||
func (c *AppAggregateController) noTenantHome(claims *jwtutil.Claims) map[string]interface{} {
|
||||
var modRows []models.SystemModules
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
Filter("is_show", 1).
|
||||
OrderBy("sort", "id").
|
||||
All(&modRows)
|
||||
modRows = services.FilterModulesByPlatform(modRows, models.PlatformApp)
|
||||
modules := make([]publicModuleItem, 0, len(modRows))
|
||||
for _, m := range modRows {
|
||||
modules = append(modules, publicModuleItem{
|
||||
ID: m.ID, Code: m.Code, Name: m.Name, Icon: m.Icon,
|
||||
Path: m.Path, Description: m.Description, RequireLogin: m.RequireLogin,
|
||||
})
|
||||
}
|
||||
|
||||
var menuRows []models.SystemMenu
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemMenu)).
|
||||
Filter("status", 1).
|
||||
Filter("require_login", 0).
|
||||
All(&menuRows)
|
||||
allModules := c.allModules()
|
||||
menus := make([]appMenuItem, 0, len(menuRows))
|
||||
for _, m := range menuRows {
|
||||
if m.Type == 3 {
|
||||
continue
|
||||
}
|
||||
item := appMenuItem{
|
||||
ID: m.ID, Pid: m.Pid, Type: m.Type, Sort: m.Sort,
|
||||
Title: strings.TrimSpace(m.Title), Leaf: true,
|
||||
}
|
||||
if m.Path != nil {
|
||||
item.Path = strings.TrimSpace(*m.Path)
|
||||
}
|
||||
if m.Icon != nil {
|
||||
item.Icon = strings.TrimSpace(*m.Icon)
|
||||
}
|
||||
item.Module = appModuleCodeOfPath(item.Path, allModules)
|
||||
menus = append(menus, item)
|
||||
}
|
||||
|
||||
state := "none"
|
||||
var apply models.AuthTenantApply
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||
Filter("identity_id", uint64(claims.UserID)).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").One(&apply); err == nil {
|
||||
switch apply.Status {
|
||||
case models.TenantApplyStatusPending:
|
||||
state = "pending"
|
||||
case models.TenantApplyStatusReject:
|
||||
state = "rejected"
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"account": claims.Username, "tenant": "", "tid": 0, "need_tenant": true,
|
||||
},
|
||||
"modules": modules,
|
||||
"menus": menus,
|
||||
"stats": map[string]interface{}{},
|
||||
"quick_menu_ids": []uint64{},
|
||||
"tenant_state": state,
|
||||
}
|
||||
}
|
||||
|
||||
// publicModuleItem 移动端免登录可见的模块卡片:只暴露展示与安全开关,不含菜单/KPI。
|
||||
// 供未登录用户渲染 nav 宫格,require_login 决定点击是「进登录引导」还是「直接进入」。
|
||||
type publicModuleItem struct {
|
||||
|
||||
@@ -111,7 +111,7 @@ func (c *AuthAppTicketController) Exchange() {
|
||||
}
|
||||
var req struct {
|
||||
Ticket string `json:"ticket"`
|
||||
Token string `json:"token"` // 兼容 mp-login 的命名
|
||||
Token string `json:"token"` // 兼容 mp-login 的命名
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
@@ -143,7 +143,7 @@ func (c *AuthAppTicketController) Exchange() {
|
||||
return
|
||||
}
|
||||
|
||||
username := "云泽用户"
|
||||
username := "温新云用户"
|
||||
if identity.Nickname != nil && strings.TrimSpace(*identity.Nickname) != "" {
|
||||
username = strings.TrimSpace(*identity.Nickname)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// 小程序登录失败原因(前端按此分支给出不同引导)
|
||||
const (
|
||||
MiniReasonNotConfigured = "not_configured" // 服务端未配置小程序
|
||||
MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到云泽账号
|
||||
MiniReasonIdentityNotFound = "identity_not_found" // 微信未匹配到温新云账号
|
||||
MiniReasonNoTenant = "no_tenant" // 账号未加入任何企业
|
||||
MiniReasonWechatAPI = "wechat_api_error" // 微信接口报错
|
||||
)
|
||||
@@ -120,7 +120,7 @@ func (c *AuthWechatMiniController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
// 定位云泽身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid
|
||||
// 定位温新云身份:手机号优先(已通过微信验证),其次查已绑定的小程序 openid
|
||||
var mobile string
|
||||
if req.PhoneCode != "" {
|
||||
phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode)
|
||||
@@ -137,7 +137,7 @@ func (c *AuthWechatMiniController) Login() {
|
||||
|
||||
identity, err := c.resolveIdentity(cfg, mobile, session)
|
||||
if err != nil {
|
||||
c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定云泽账号,请使用手机号一键登录或先注册")
|
||||
c.fail(MiniReasonIdentityNotFound, 404, "该微信未绑定温新云账号,请使用手机号一键登录或先注册")
|
||||
return
|
||||
}
|
||||
if identity.Status != models.AuthIdentityStatusEnabled {
|
||||
@@ -147,15 +147,18 @@ func (c *AuthWechatMiniController) Login() {
|
||||
|
||||
// 企业选择:单企业直接进入,多企业签发待选令牌由前端展示选择界面
|
||||
tenants, lerr := authsvc.ListTenantOptions(identity.ID)
|
||||
if lerr != nil || len(tenants) == 0 {
|
||||
c.fail(MiniReasonNoTenant, 403, "该账号未加入任何企业,请联系管理员")
|
||||
if lerr != nil {
|
||||
c.fail("", 400, "获取企业列表失败,请重试")
|
||||
return
|
||||
}
|
||||
// 新流程:账号已注册但尚未加入企业时仍签发正式令牌(tid=0),
|
||||
// 端上可继续使用原免登录模块,并在进入需企业模块时引导创建/挂靠企业。
|
||||
noTenant := len(tenants) == 0
|
||||
tid := uint64(0)
|
||||
needChoose := false
|
||||
if len(tenants) == 1 {
|
||||
tid = tenants[0].Tid
|
||||
} else {
|
||||
} else if len(tenants) > 1 {
|
||||
tid = authsvc.PendingTenantID
|
||||
needChoose = true
|
||||
}
|
||||
@@ -174,11 +177,12 @@ func (c *AuthWechatMiniController) Login() {
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"tokens": tokens,
|
||||
"tenants": tenants,
|
||||
"identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)},
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"tokens": tokens,
|
||||
"tenants": tenants,
|
||||
"identity": map[string]interface{}{"id": identity.ID, "nickname": username, "mobile": mobileOr(identity)},
|
||||
"need_tenant": noTenant,
|
||||
}
|
||||
if needChoose {
|
||||
resp["need_choose_tenant"] = true
|
||||
|
||||
@@ -114,7 +114,7 @@ func (c *GuestController) MiniLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
// 手机号已注册云泽账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。
|
||||
// 手机号已注册温新云账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。
|
||||
// (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑)
|
||||
if c.tryUserLogin(mobile, session) {
|
||||
return
|
||||
|
||||
@@ -87,11 +87,11 @@ func (c *IndexPortalController) GetHeadMenu() {
|
||||
// 使用 OR 逻辑查询 name 或 short_name
|
||||
cond := orm.NewCondition()
|
||||
cond = cond.Or("tenant_name", tenantName).Or("tenant_short_name", tenantName)
|
||||
|
||||
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
SetCond(cond).
|
||||
One(&tenant)
|
||||
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] QueryTenant Error: %v, Looking for: %s\n", err, tenantName)
|
||||
} else {
|
||||
@@ -206,9 +206,9 @@ func (c *IndexPortalController) GetFooterData() {
|
||||
|
||||
// 准备默认的 Footer 配置数据
|
||||
footerMap := map[string]string{
|
||||
"sitename": "云泽官方网站",
|
||||
"companyname": "云泽智能科技有限公司",
|
||||
"copyright": "Copyright © 2026 Yunzer. All Rights Reserved.",
|
||||
"sitename": "温新云官方网站",
|
||||
"companyname": "连云港温新智能科技有限公司",
|
||||
"copyright": "Copyright © 2026 WENXIN. All Rights Reserved.",
|
||||
"icp": "苏ICP备2026000000号-1",
|
||||
"description": "专业为企业建设数字化管理与官网门户的一站式 SaaS 服务平台",
|
||||
"logo": "",
|
||||
@@ -485,4 +485,4 @@ func (c *IndexPortalController) GetOnePageByPath() {
|
||||
"data": nil,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package controllers
|
||||
|
||||
// ==================== 小程序新用户引导 ====================
|
||||
//
|
||||
// 新的工作台口径:不再区分「登录 / 免登录」。
|
||||
// 1. 点任意模块卡片 → 微信手机号授权;
|
||||
// 2. 手机号已存在 → 前端走 /auth/wechat/miniapp/login 静默登录(自动绑定 openid);
|
||||
// 不存在 → 本文件 Phone 签发注册票据,Register 收 ≥6 位密码后建号并绑定微信,
|
||||
// 同时签发 tid=0 的正式令牌(注册即登录,可直接使用原免登录模块);
|
||||
// 3. 进入需企业模块前,Status 查企业绑定 / 申请状态;未通过则走创建企业或挂靠企业;
|
||||
// 4. 两类申请复用 services 层已有的审核闭环(platform / backend 审批端无需改动)。
|
||||
//
|
||||
// 本组路由均为公开路径(不在 middleware.protectedPrefixes 内):
|
||||
// phone / register 由微信凭证与一次性注册票据自证,其余端点自行校验 Bearer 令牌。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/identity"
|
||||
"server/pkg/jwtutil"
|
||||
"server/pkg/passwordutil"
|
||||
"server/services"
|
||||
authsvc "server/services/auth"
|
||||
"server/services/wechatmini"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
const onboardingClientID = "yz-miniapp"
|
||||
|
||||
type OnboardingController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *OnboardingController) reply(status int, msg string, data interface{}) {
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Data["json"] = map[string]interface{}{"code": status, "msg": msg, "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *OnboardingController) ok(data interface{}) {
|
||||
c.reply(200, "success", data)
|
||||
}
|
||||
|
||||
func (c *OnboardingController) fail(status int, msg string) {
|
||||
c.reply(status, msg, nil)
|
||||
}
|
||||
|
||||
func (c *OnboardingController) readJSON(v interface{}) bool {
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||||
}
|
||||
if err := json.Unmarshal(body, v); err != nil {
|
||||
c.fail(400, "参数错误")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// claims 当前登录态;允许 tid=0(刚注册、尚未加入企业)的正式令牌。
|
||||
func (c *OnboardingController) claims() (*jwtutil.Claims, error) {
|
||||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp)
|
||||
}
|
||||
|
||||
func (c *OnboardingController) mustClaims() *jwtutil.Claims {
|
||||
cl, err := c.claims()
|
||||
if err != nil || cl == nil || cl.UserID <= 0 {
|
||||
c.fail(401, "登录态已失效,请重新授权手机号")
|
||||
return nil
|
||||
}
|
||||
return cl
|
||||
}
|
||||
|
||||
// Phone POST /api/onboarding/phone
|
||||
// body: { code(wx.login), phone_code(getPhoneNumber), appid? }
|
||||
// 返回 exists / need_register;无账号时附带一次性 register_token(内含微信手机号与 openid)。
|
||||
func (c *OnboardingController) Phone() {
|
||||
var req struct {
|
||||
Code string `json:"code"`
|
||||
PhoneCode string `json:"phone_code"`
|
||||
AppID string `json:"appid"`
|
||||
}
|
||||
if !c.readJSON(&req) {
|
||||
return
|
||||
}
|
||||
cfg, err := loadGuestMiniConfig(req.AppID)
|
||||
if err != nil {
|
||||
c.fail(400, "小程序未配置,无法获取手机号")
|
||||
return
|
||||
}
|
||||
phone, perr := wechatmini.GetPhoneNumber(cfg, strings.TrimSpace(req.PhoneCode))
|
||||
if perr != nil {
|
||||
msg := perr.Error()
|
||||
if hint, ok := wechatmini.ExplainError(perr); ok {
|
||||
msg = hint
|
||||
}
|
||||
logs.Warn("[onboarding] 获取手机号失败: %v", perr)
|
||||
c.fail(400, "手机号授权失败:"+msg)
|
||||
return
|
||||
}
|
||||
mobile := strings.TrimSpace(phone.PurePhoneNumber)
|
||||
if mobile == "" {
|
||||
mobile = strings.TrimSpace(phone.PhoneNumber)
|
||||
}
|
||||
if mobile == "" {
|
||||
c.fail(400, "未获取到手机号")
|
||||
return
|
||||
}
|
||||
|
||||
// 已注册:交给前端用现有小程序一键登录静默登录(自动补绑 openid)
|
||||
if exist := identityByPhone(mobile); exist != nil {
|
||||
c.ok(map[string]interface{}{
|
||||
"exists": true,
|
||||
"need_register": false,
|
||||
"mobile_mask": maskPhone(mobile),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 注册必须同时绑定微信号:拿不到 openid 时不签发注册票据,避免留下「无绑定」账号
|
||||
session := guestMiniSession(cfg, req.Code)
|
||||
if session == nil || strings.TrimSpace(session.OpenID) == "" {
|
||||
c.fail(400, "微信身份获取失败,请重新进入小程序授权")
|
||||
return
|
||||
}
|
||||
token := services.IssuePendingRegisterToken(services.RegisterPending{
|
||||
Mobile: mobile,
|
||||
OpenID: session.OpenID,
|
||||
UnionID: session.UnionID,
|
||||
Provider: models.IdPWechatMini,
|
||||
})
|
||||
if token == "" {
|
||||
c.fail(500, "注册会话创建失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"exists": false,
|
||||
"need_register": true,
|
||||
"register_token": token,
|
||||
"mobile_mask": maskPhone(mobile),
|
||||
})
|
||||
}
|
||||
|
||||
// Register POST /api/onboarding/register
|
||||
// body: { register_token, password, confirm_password }
|
||||
// 密码最少 6 位;复用 CompleteRegister 建号 + 绑定微信,随后签发 tid=0 正式令牌。
|
||||
func (c *OnboardingController) Register() {
|
||||
var req struct {
|
||||
Token string `json:"register_token"`
|
||||
Password string `json:"password"`
|
||||
Confirm string `json:"confirm_password"`
|
||||
}
|
||||
if !c.readJSON(&req) {
|
||||
return
|
||||
}
|
||||
tk := strings.TrimSpace(req.Token)
|
||||
if tk == "" {
|
||||
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||
return
|
||||
}
|
||||
pwd := strings.TrimSpace(req.Password)
|
||||
if len(pwd) < 6 {
|
||||
c.fail(400, "密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
if pwd != strings.TrimSpace(req.Confirm) {
|
||||
c.fail(400, "两次输入的密码不一致")
|
||||
return
|
||||
}
|
||||
hashed, herr := passwordutil.Hash(pwd)
|
||||
if herr != nil {
|
||||
c.fail(500, "密码处理失败,请重试")
|
||||
return
|
||||
}
|
||||
|
||||
var session models.AuthRegisterSession
|
||||
if err := models.Orm.QueryTable(new(models.AuthRegisterSession)).Filter("token", tk).One(&session); err != nil {
|
||||
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||
return
|
||||
}
|
||||
if time.Now().After(session.ExpireAt) {
|
||||
c.fail(400, "注册会话已过期,请重新授权手机号")
|
||||
return
|
||||
}
|
||||
num, uerr := models.Orm.QueryTable(new(models.AuthRegisterSession)).
|
||||
Filter("token", tk).
|
||||
Update(map[string]interface{}{
|
||||
"password_hash": hashed,
|
||||
"password_algo": passwordutil.AlgoOf(hashed),
|
||||
"update_time": time.Now(),
|
||||
})
|
||||
if uerr != nil || num == 0 {
|
||||
c.fail(400, "注册会话已失效,请重新授权手机号")
|
||||
return
|
||||
}
|
||||
|
||||
identityID, err := services.CompleteRegister(tk)
|
||||
if err != nil {
|
||||
c.fail(400, err.Error())
|
||||
return
|
||||
}
|
||||
// 注册会话一次性使用:账号已建,后续申请凭 Bearer 令牌
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthRegisterSession)).Filter("token", tk).Delete()
|
||||
|
||||
tokens, terr := c.issueNoTenantTokens(identityID, session.Mobile)
|
||||
if terr != nil {
|
||||
logs.Error("[onboarding] 签发无企业令牌失败: %v", terr)
|
||||
c.fail(500, "注册成功但登录态签发失败,请重新进入小程序")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"tokens": tokens,
|
||||
"need_tenant": true,
|
||||
"mobile_mask": maskPhone(session.Mobile),
|
||||
})
|
||||
}
|
||||
|
||||
// issueNoTenantTokens 注册即登录:签发 tid=0、userType=app 的正式令牌。
|
||||
// /api/home 对 tid=0 返回公开模块与公开菜单,因此原「免登录」功能可直接使用。
|
||||
func (c *OnboardingController) issueNoTenantTokens(identityID uint64, mobile string) (*authsvc.TokenPair, error) {
|
||||
sess, err := authsvc.CreateSession(authsvc.SessionInfo{
|
||||
IdentityID: identityID,
|
||||
Tid: 0,
|
||||
ClientID: onboardingClientID,
|
||||
IP: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Request.UserAgent(),
|
||||
LoginType: "wechat_phone",
|
||||
Amr: "wechat_phone",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authsvc.IssueTokens(authsvc.TokenIssue{
|
||||
IdentityID: identityID,
|
||||
Tid: 0,
|
||||
ClientID: onboardingClientID,
|
||||
Sid: sess.Sid,
|
||||
Username: maskPhone(mobile),
|
||||
UserType: "app",
|
||||
Amr: "wechat_phone",
|
||||
})
|
||||
}
|
||||
|
||||
// Status GET /api/onboarding/status
|
||||
// 返回企业绑定、最新申请状态、微信手机号(前端只读展示)。
|
||||
func (c *OnboardingController) Status() {
|
||||
cl := c.mustClaims()
|
||||
if cl == nil {
|
||||
return
|
||||
}
|
||||
identityID := uint64(cl.UserID)
|
||||
|
||||
var ai models.AuthIdentity
|
||||
mobile := ""
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", identityID).Filter("delete_time__isnull", true).One(&ai); err == nil && ai.Mobile != nil {
|
||||
mobile = *ai.Mobile
|
||||
}
|
||||
|
||||
var binds []models.AuthTenantUser
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("status", 1).
|
||||
All(&binds)
|
||||
hasTenant := len(binds) > 0
|
||||
tenantName := ""
|
||||
tid := uint64(0)
|
||||
if hasTenant {
|
||||
tid = binds[0].Tid
|
||||
var t models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", tid).One(&t); err == nil {
|
||||
tenantName = t.TenantName
|
||||
}
|
||||
}
|
||||
|
||||
var applyData interface{}
|
||||
var apply models.AuthTenantApply
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").One(&apply); err == nil {
|
||||
applyData = map[string]interface{}{
|
||||
"id": apply.ID,
|
||||
"type": apply.Type,
|
||||
"status": apply.Status,
|
||||
"tenant_name": apply.TenantName,
|
||||
"reject_reason": apply.RejectReason,
|
||||
"department_name": apply.DepartmentName,
|
||||
"position_name": apply.PositionName,
|
||||
"applicant_name": apply.ApplicantName,
|
||||
}
|
||||
}
|
||||
|
||||
c.ok(map[string]interface{}{
|
||||
"has_tenant": hasTenant,
|
||||
"tid": tid,
|
||||
"tenant_name": tenantName,
|
||||
"mobile": mobile,
|
||||
"mobile_mask": maskPhone(mobile),
|
||||
"apply": applyData,
|
||||
})
|
||||
}
|
||||
|
||||
// Tenants GET /api/onboarding/tenants?keyword=xxx
|
||||
// 挂靠企业:按企业全称搜索启用中的企业(复用注册页的企业联想)。
|
||||
func (c *OnboardingController) Tenants() {
|
||||
items, err := services.SearchTenantsForRegister(c.GetString("keyword"))
|
||||
if err != nil {
|
||||
c.fail(500, "企业搜索失败")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": items})
|
||||
}
|
||||
|
||||
// Departments GET /api/onboarding/departments?tid=xxx
|
||||
// 公开下拉:按企业返回组织(部门)列表。挂靠时用户尚未加入该企业,故不走 /backend 鉴权接口。
|
||||
func (c *OnboardingController) Departments() {
|
||||
tid := c.uintParam("tid")
|
||||
if tid == 0 {
|
||||
c.fail(400, "请先选择企业")
|
||||
return
|
||||
}
|
||||
var rows []models.BackendOrganization
|
||||
_, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.fail(500, "部门列表读取失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID, "name": r.OrgName, "parent_id": r.ParentID, "is_company": r.IsCompany,
|
||||
})
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Positions GET /api/onboarding/positions?tid=xxx&department_id=xxx
|
||||
func (c *OnboardingController) Positions() {
|
||||
tid := c.uintParam("tid")
|
||||
if tid == 0 {
|
||||
c.fail(400, "请先选择企业")
|
||||
return
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.BackendPosition)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true)
|
||||
if deptID := c.uintParam("department_id"); deptID > 0 {
|
||||
qs = qs.Filter("department_id", deptID)
|
||||
}
|
||||
var rows []models.BackendPosition
|
||||
if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil {
|
||||
c.fail(500, "职位列表读取失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID, "name": r.PositionName, "department_id": r.DepartmentID,
|
||||
})
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// ApplyJoin POST /api/onboarding/apply-join
|
||||
// body: { tid, applicant_name, department_id, department_name, position_id, position_name }
|
||||
// 手机号不接受前端传值:由后端从微信验证过的身份手机号写入 wechat_mobile。
|
||||
func (c *OnboardingController) ApplyJoin() {
|
||||
cl := c.mustClaims()
|
||||
if cl == nil {
|
||||
return
|
||||
}
|
||||
if c.hasTenant(uint64(cl.UserID)) {
|
||||
c.fail(400, "你已加入企业,无需重复申请")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
ApplicantName string `json:"applicant_name"`
|
||||
DepartmentID uint64 `json:"department_id"`
|
||||
DepartmentName string `json:"department_name"`
|
||||
PositionID uint64 `json:"position_id"`
|
||||
PositionName string `json:"position_name"`
|
||||
}
|
||||
if !c.readJSON(&req) {
|
||||
return
|
||||
}
|
||||
if req.Tid == 0 || strings.TrimSpace(req.ApplicantName) == "" || req.DepartmentID == 0 || req.PositionID == 0 {
|
||||
c.fail(400, "请填写姓名并选择企业、部门、职位")
|
||||
return
|
||||
}
|
||||
mobile := c.identityMobile(uint64(cl.UserID))
|
||||
apply, err := services.CreateJoinApply(uint64(cl.UserID), req.Tid, services.JoinApplyDetail{
|
||||
ApplicantName: req.ApplicantName,
|
||||
DepartmentID: req.DepartmentID,
|
||||
DepartmentName: req.DepartmentName,
|
||||
PositionID: req.PositionID,
|
||||
PositionName: req.PositionName,
|
||||
WechatMobile: mobile,
|
||||
})
|
||||
if err != nil {
|
||||
c.fail(400, err.Error())
|
||||
return
|
||||
}
|
||||
// 微信手机号单独落库,避免被表单联系人字段覆盖
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthTenantApply)).
|
||||
Filter("id", apply.ID).
|
||||
Update(map[string]interface{}{"wechat_mobile": mobile})
|
||||
c.ok(map[string]interface{}{"apply_id": apply.ID, "status": apply.Status})
|
||||
}
|
||||
|
||||
// ApplyCreate POST /api/onboarding/apply-create
|
||||
// 沿用 PC 注册页的完整字段与校验(信用代码 18 位、营业执照、法人、联系人、电话、邮箱必填)。
|
||||
func (c *OnboardingController) ApplyCreate() {
|
||||
cl := c.mustClaims()
|
||||
if cl == nil {
|
||||
return
|
||||
}
|
||||
if c.hasTenant(uint64(cl.UserID)) {
|
||||
c.fail(400, "你已加入企业,无需重复申请")
|
||||
return
|
||||
}
|
||||
var req services.TenantCreateApplyInput
|
||||
if !c.readJSON(&req) {
|
||||
return
|
||||
}
|
||||
apply, err := services.CreateTenantApply(uint64(cl.UserID), req)
|
||||
if err != nil {
|
||||
c.fail(400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"apply_id": apply.ID, "status": apply.Status})
|
||||
}
|
||||
|
||||
func (c *OnboardingController) hasTenant(identityID uint64) bool {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("identity_id", identityID).
|
||||
Filter("status", 1).
|
||||
Count()
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
func (c *OnboardingController) identityMobile(identityID uint64) string {
|
||||
var ai models.AuthIdentity
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", identityID).Filter("delete_time__isnull", true).One(&ai); err != nil {
|
||||
return ""
|
||||
}
|
||||
if ai.Mobile == nil {
|
||||
return ""
|
||||
}
|
||||
return *ai.Mobile
|
||||
}
|
||||
|
||||
func (c *OnboardingController) uintParam(key string) uint64 {
|
||||
v, _ := strconv.ParseUint(strings.TrimSpace(c.GetString(key)), 10, 64)
|
||||
return v
|
||||
}
|
||||
@@ -33,7 +33,6 @@ func (c *PlatformWebsiteSettingController) jsonErr(httpStatus, bizCode int, msg
|
||||
// Info GET /platform/website/setting/info
|
||||
// 直接把表里所有设置项返回,前端按行渲染(新增设置项无需改代码):
|
||||
// data.list = [{ id, title, value, tag, remark }]
|
||||
// data.values = { "sitename": "云泽科技", ... }(key 即标签名,方便直接使用)
|
||||
func (c *PlatformWebsiteSettingController) Info() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
code := identity.Status(err)
|
||||
@@ -65,7 +64,6 @@ func (c *PlatformWebsiteSettingController) Info() {
|
||||
}
|
||||
|
||||
// Save POST /platform/website/setting/save
|
||||
// 入参 { "sitename": "云泽科技", "subtitle": "...", ... },key 为标签名,按标签逐项 upsert。
|
||||
// 通用:表里已存在或新增的任意合法标签都可写入,不需要登记。
|
||||
func (c *PlatformWebsiteSettingController) Save() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- ============================================================
|
||||
-- 小程序「挂靠企业」申请表补列
|
||||
-- 前置:yz_auth_tenant_apply 已由 create_auth_tenant_apply.sql 创建。
|
||||
-- 幂等说明:MySQL 5.7/8.0 不支持 ADD COLUMN IF NOT EXISTS,
|
||||
-- 重复执行会报 Duplicate column,可忽略(先 SELECT 确认)。
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE `yz_auth_tenant_apply`
|
||||
ADD COLUMN `applicant_name` varchar(64) NOT NULL DEFAULT '' COMMENT '申请人姓名(挂靠表单填写)' AFTER `identity_id`,
|
||||
ADD COLUMN `department_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '部门ID(yz_backend_organization.id)' AFTER `applicant_name`,
|
||||
ADD COLUMN `department_name` varchar(128) NOT NULL DEFAULT '' COMMENT '部门名称快照' AFTER `department_id`,
|
||||
ADD COLUMN `position_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '职位ID(yz_backend_position.id)' AFTER `department_name`,
|
||||
ADD COLUMN `position_name` varchar(128) NOT NULL DEFAULT '' COMMENT '职位名称快照' AFTER `position_id`,
|
||||
ADD COLUMN `wechat_mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '微信验证手机号(只读,后端写入)' AFTER `position_name`;
|
||||
@@ -84,5 +84,5 @@ WHERE @notice_pid IS NOT NULL
|
||||
INSERT INTO `yz_platform_website_notice`
|
||||
(`title`, `summary`, `content`, `type`, `is_popup`, `top`, `sort`, `status`, `publish_time`)
|
||||
VALUES
|
||||
('云泽科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。',
|
||||
'<p>云泽科技官网全新改版上线,本次改版聚焦信息架构与访问体验。</p>', 1, 1, 1, 0, 1, NOW());
|
||||
('温新云科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。',
|
||||
'<p>温新云科技官网全新改版上线,本次改版聚焦信息架构与访问体验。</p>', 1, 1, 1, 0, 1, NOW());
|
||||
@@ -1,2 +0,0 @@
|
||||
merge_key,row_count,password_variants,tids,representative_id
|
||||
m:19895983967,2,2,1|2,4
|
||||
|
@@ -1,2 +0,0 @@
|
||||
merge_key,row_count,password_variants,tids,representative_id
|
||||
m:19895983967,2,2,1|2,4
|
||||
|
@@ -1,4 +0,0 @@
|
||||
old_id,old_uid,tid,identity_id,tenant_user_id,account,merge_key,conflict
|
||||
2,96085169,2,1,1,test1,m:13357867407,false
|
||||
1,88888888,1,2,2,hero920103,m:19895983967,true
|
||||
4,67091493,2,2,3,hero920103,m:19895983967,true
|
||||
|
@@ -22,6 +22,13 @@ const (
|
||||
type AuthTenantApply struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
IdentityID uint64 `orm:"column(identity_id)" json:"identity_id"`
|
||||
// 小程序「挂靠企业」填写的资料:姓名 / 部门 / 职位 / 微信手机号(只读,后端取身份手机号)
|
||||
ApplicantName string `orm:"column(applicant_name);size(64);default('')" json:"applicant_name"`
|
||||
DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"`
|
||||
DepartmentName string `orm:"column(department_name);size(128);default('')" json:"department_name"`
|
||||
PositionID uint64 `orm:"column(position_id);default(0)" json:"position_id"`
|
||||
PositionName string `orm:"column(position_name);size(128);default('')" json:"position_name"`
|
||||
WechatMobile string `orm:"column(wechat_mobile);size(20);default('')" json:"wechat_mobile"`
|
||||
Type int8 `orm:"column(type);default(1)" json:"type"`
|
||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||
TenantName string `orm:"column(tenant_name);size(128);default('')" json:"tenant_name"`
|
||||
|
||||
Binary file not shown.
@@ -185,4 +185,16 @@ func Register() {
|
||||
beego.Router("/api/guest/data/:id", &controllers.GuestController{}, "put:DataUpdate;delete:DataDelete")
|
||||
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
||||
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
||||
|
||||
// ===== 小程序新用户引导:手机号 → 设密码注册 → 创建 / 挂靠企业 =====
|
||||
// 公开路径(不在 jwt 保护前缀内):phone / register 由微信凭证与注册票据自证,
|
||||
// 其余端点由控制器自行校验 Bearer 令牌(含 tid=0 的未加入企业令牌)。
|
||||
beego.Router("/api/onboarding/phone", &controllers.OnboardingController{}, "post:Phone")
|
||||
beego.Router("/api/onboarding/register", &controllers.OnboardingController{}, "post:Register")
|
||||
beego.Router("/api/onboarding/status", &controllers.OnboardingController{}, "get:Status")
|
||||
beego.Router("/api/onboarding/tenants", &controllers.OnboardingController{}, "get:Tenants")
|
||||
beego.Router("/api/onboarding/departments", &controllers.OnboardingController{}, "get:Departments")
|
||||
beego.Router("/api/onboarding/positions", &controllers.OnboardingController{}, "get:Positions")
|
||||
beego.Router("/api/onboarding/apply-join", &controllers.OnboardingController{}, "post:ApplyJoin")
|
||||
beego.Router("/api/onboarding/apply-create", &controllers.OnboardingController{}, "post:ApplyCreate")
|
||||
}
|
||||
@@ -436,8 +436,20 @@ func SearchTenantsForRegister(keyword string) ([]TenantSearchItem, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// JoinApplyDetail 小程序「挂靠企业」补充资料。
|
||||
// 手机号不允许前端传:统一由后端从身份表读取微信验证过的手机号。
|
||||
type JoinApplyDetail struct {
|
||||
ApplicantName string
|
||||
DepartmentID uint64
|
||||
DepartmentName string
|
||||
PositionID uint64
|
||||
PositionName string
|
||||
WechatMobile string
|
||||
}
|
||||
|
||||
// CreateJoinApply 「绑定企业」申请:等待目标企业管理员审批
|
||||
func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) {
|
||||
// detail 为可选参数,PC 两步式注册沿用原调用(不传),小程序挂靠表单传入补充资料。
|
||||
func CreateJoinApply(identityID, tid uint64, detail ...JoinApplyDetail) (*models.AuthTenantApply, error) {
|
||||
if identityID == 0 || tid == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
@@ -477,7 +489,22 @@ func CreateJoinApply(identityID, tid uint64) (*models.AuthTenantApply, error) {
|
||||
TenantName: tenant.TenantName,
|
||||
Status: models.TenantApplyStatusPending,
|
||||
}
|
||||
apply.ContactPerson = identityNickname(identity)
|
||||
// 小程序挂靠表单:补充姓名 / 部门 / 职位,手机号取微信验证过的身份手机号
|
||||
if len(detail) > 0 {
|
||||
d := detail[0]
|
||||
apply.ApplicantName = strings.TrimSpace(d.ApplicantName)
|
||||
apply.DepartmentID = d.DepartmentID
|
||||
apply.DepartmentName = strings.TrimSpace(d.DepartmentName)
|
||||
apply.PositionID = d.PositionID
|
||||
apply.PositionName = strings.TrimSpace(d.PositionName)
|
||||
apply.WechatMobile = strings.TrimSpace(d.WechatMobile)
|
||||
if apply.ApplicantName != "" {
|
||||
apply.ContactPerson = apply.ApplicantName
|
||||
}
|
||||
}
|
||||
if apply.ContactPerson == "" {
|
||||
apply.ContactPerson = identityNickname(identity)
|
||||
}
|
||||
if identity.Mobile != nil {
|
||||
apply.ContactPhone = *identity.Mobile
|
||||
}
|
||||
@@ -622,6 +649,13 @@ func ApproveJoinApply(applyID, tid, reviewerID uint64, reviewerName string) erro
|
||||
if err := bindIdentityToTenant(identity, tid, models.GetTenantUserRole); err != nil {
|
||||
return err
|
||||
}
|
||||
// 挂靠申请选了部门:审批通过后把 org_id 落到认证中心绑定行
|
||||
if apply.DepartmentID > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("identity_id", apply.IdentityID).
|
||||
Update(map[string]interface{}{"org_id": apply.DepartmentID, "update_time": time.Now()})
|
||||
}
|
||||
return finishApply(apply, models.TenantApplyStatusPassed, "", reviewerID, reviewerName)
|
||||
}
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ func (s *WechatMpSender) Send(ctx context.Context, reminder ReminderData, title,
|
||||
perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{
|
||||
Title: title,
|
||||
Content: content,
|
||||
Remark: "来自云泽管理平台日程提醒",
|
||||
Remark: "来自温新云平台日程提醒",
|
||||
})
|
||||
if perr != nil {
|
||||
return false, perr
|
||||
|
||||
@@ -46,6 +46,8 @@ func (e *APIError) Explain() string {
|
||||
return "AppID 无效:请核对小程序后台「开发 → 开发管理」中的 AppID"
|
||||
case 40125:
|
||||
return "AppSecret 错误:请在小程序后台重置后复制新的 AppSecret 填入本页"
|
||||
case 40164, 61004:
|
||||
return "服务器出口 IP 不在白名单:请在小程序后台「开发管理 → 开发设置 → IP白名单」加入本服务器出口 IP"
|
||||
case 40226:
|
||||
return "该小程序未开通手机号快速验证能力:请在「开发管理 → 接口设置」中开通"
|
||||
case 45009:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -187,7 +187,7 @@
|
||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="brand-name">云泽统一认证中心</span>
|
||||
<span class="brand-name">温新云统一认证中心</span>
|
||||
</div>
|
||||
|
||||
<div class="side-title">找回登录密码</div>
|
||||
|
||||
@@ -560,7 +560,7 @@
|
||||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="brand-name">云泽统一认证中心</span>
|
||||
<span class="brand-name">温新云统一认证中心</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="mobile-brand-text">
|
||||
<span class="mobile-brand-name">云泽统一认证中心</span>
|
||||
<span class="mobile-brand-name">温新云统一认证中心</span>
|
||||
<span class="mobile-brand-sub">{{.LoginTitle}}</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -769,7 +769,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="qr-title">微信扫码登录</p>
|
||||
<p class="qr-desc">使用微信「扫一扫」,扫码后在<strong>云泽小程序</strong>中点击确认<br />即可完成登录</p>
|
||||
<p class="qr-desc">使用微信「扫一扫」,扫码后在<strong>温新云小程序</strong>中点击确认<br />即可完成登录</p>
|
||||
<p class="qr-note">二维码 5 分钟内有效,过期可点击刷新</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -799,7 +799,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-foot">
|
||||
<span>© 2026 云泽科技 版权所有</span>
|
||||
<span>© 2026 云泽 版权所有</span>
|
||||
<button id="cacheClearBtn" class="cache-clear-btn" type="button" onclick="clearSiteCache()" title="仅清除本站在当前浏览器中的缓存数据">清除缓存</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in new issue
Block a user