主要更新uniapp相关功能
This commit is contained in:
1 parent
aa92ca40a2
commit
5b4dbb2b46
65 files changed
+8532
-1921
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
@@ -53,9 +55,9 @@ type appMenuItem struct {
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Type int8 `json:"type"`
|
||||
Sort int64 `json:"sort"`
|
||||
Module string `json:"module"` // 归属模块 code(crm/erp/oa/cms/ai),公共菜单为空
|
||||
Locked bool `json:"locked"` // true = 不在租户套餐内,前端置灰并引导升级
|
||||
Leaf bool `json:"leaf"` // true = 叶子菜单(无下级),可直接跳转
|
||||
Module string `json:"module"` // 归属模块 code(crm/erp/oa/cms/ai),公共菜单为空
|
||||
Locked bool `json:"locked"` // true = 不在租户套餐内,前端置灰并引导升级
|
||||
Leaf bool `json:"leaf"` // true = 叶子菜单(无下级),可直接跳转
|
||||
Docs int `json:"docs,omitempty"` // 该节点下可见的子菜单数量
|
||||
}
|
||||
|
||||
@@ -128,25 +130,100 @@ func (c *AppAggregateController) Home() {
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"user": c.appUser(claims, uid, tid),
|
||||
"modules": modules,
|
||||
"menus": menus,
|
||||
"stats": c.appStats(claims, tid),
|
||||
"user": c.appUser(claims, uid, tid),
|
||||
"modules": modules,
|
||||
"menus": menus,
|
||||
"stats": c.appStats(claims, tid),
|
||||
"quick_menu_ids": c.appQuickMenuIds(tid, uint64(claims.UserID)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// appQuickMenuIds 读取当前用户自定义的常用功能菜单 ID 列表(有序)。
|
||||
// 未设置或解析失败一律返回空数组,前端据此回落到默认取前 N 个叶子菜单。
|
||||
func (c *AppAggregateController) appQuickMenuIds(tid, uid uint64) []uint64 {
|
||||
out := []uint64{}
|
||||
var row models.UniappUserQuickMenu
|
||||
if err := models.Orm.QueryTable(new(models.UniappUserQuickMenu)).
|
||||
Filter("tid", tid).Filter("uid", uid).One(&row); err != nil {
|
||||
return out
|
||||
}
|
||||
raw := strings.TrimSpace(row.MenuIds)
|
||||
if raw == "" {
|
||||
return out
|
||||
}
|
||||
var ids []uint64
|
||||
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
|
||||
return out
|
||||
}
|
||||
if ids == nil {
|
||||
return out
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// SaveQuickMenus POST /app/quick-menus
|
||||
//
|
||||
// 全量覆盖保存当前用户的常用功能菜单 ID 列表。请求体:{ "menu_ids": [81,102,...] }。
|
||||
// 顺序即展示顺序;传空数组表示清空自定义(前端回落到默认)。
|
||||
func (c *AppAggregateController) SaveQuickMenus() {
|
||||
claims, err := c.appClaims()
|
||||
if err != nil {
|
||||
c.fail(err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
uid := uint64(claims.UserID)
|
||||
|
||||
var body struct {
|
||||
MenuIds []uint64 `json:"menu_ids"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请求体格式错误"})
|
||||
return
|
||||
}
|
||||
if body.MenuIds == nil {
|
||||
body.MenuIds = []uint64{}
|
||||
}
|
||||
raw, _ := json.Marshal(body.MenuIds)
|
||||
|
||||
now := time.Now()
|
||||
var row models.UniappUserQuickMenu
|
||||
if err := models.Orm.QueryTable(new(models.UniappUserQuickMenu)).
|
||||
Filter("tid", tid).Filter("uid", uid).One(&row); err == nil {
|
||||
_, _ = models.Orm.QueryTable(new(models.UniappUserQuickMenu)).
|
||||
Filter("id", row.ID).Update(orm.Params{
|
||||
"menu_ids": string(raw),
|
||||
"update_time": now,
|
||||
})
|
||||
} else {
|
||||
_, _ = models.Orm.Insert(&models.UniappUserQuickMenu{
|
||||
Tid: tid,
|
||||
Uid: uid,
|
||||
MenuIds: string(raw),
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
})
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"menu_ids": body.MenuIds},
|
||||
})
|
||||
}
|
||||
|
||||
// appUser 当前登录用户的展示信息(含企业名,供首页问候语使用)
|
||||
func (c *AppAggregateController) appUser(claims *jwtutil.Claims, uid, tid uint64) map[string]interface{} {
|
||||
out := map[string]interface{}{
|
||||
"uid": uid,
|
||||
"tid": tid,
|
||||
"name": "用户",
|
||||
"account": "",
|
||||
"role_name": "",
|
||||
"tenant": "",
|
||||
"tenant_id": tid,
|
||||
"avatar": "",
|
||||
"uid": uid,
|
||||
"tid": tid,
|
||||
"name": "用户",
|
||||
"account": "",
|
||||
"role_name": "",
|
||||
"tenant": "",
|
||||
"tenant_id": tid,
|
||||
"avatar": "",
|
||||
}
|
||||
if claims.Username != "" {
|
||||
out["name"] = claims.Username
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// 移动端通讯录(企业内部通讯录)只读接口。
|
||||
//
|
||||
// 复用 AppOaController 的鉴权与响应封装(oaClaims / oaErr / oaOk):
|
||||
// 鉴权放宽到 app / platform 令牌,数据一律按 JWT 中的 TenantId 隔离,前端无法指定租户。
|
||||
// 写入能力(同步员工 / 新增 / 编辑 / 删除)仍留在 Web 管理端。
|
||||
|
||||
// appContactDTO 通讯录条目 DTO(移动端展示字段)。
|
||||
func appContactDTO(row models.BackendErpContact, orgName string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"org_id": contactDerefUint64(row.OrgID),
|
||||
"org_name": orgName,
|
||||
"contact_type": row.ContactType,
|
||||
"contact_name": row.ContactName,
|
||||
"gender": row.Gender,
|
||||
"phone": contactDerefString(row.Phone),
|
||||
"mobiles": contactDerefString(row.Mobiles),
|
||||
"work_phone": contactDerefString(row.WorkPhone),
|
||||
"email": contactDerefString(row.Email),
|
||||
"wechat": contactDerefString(row.Wechat),
|
||||
"qq": contactDerefString(row.QQ),
|
||||
"avatar": contactDerefString(row.Avatar),
|
||||
"company_name": contactDerefString(row.CompanyName),
|
||||
"dept_name": contactDerefString(row.DeptName),
|
||||
"position_title": contactDerefString(row.PositionTitle),
|
||||
"address": contactDerefString(row.Address),
|
||||
"remark": contactDerefString(row.Remark),
|
||||
"is_starred": row.IsStarred,
|
||||
"status": row.Status,
|
||||
}
|
||||
}
|
||||
|
||||
// appContactOrgNames 批量查询组织 ID → 名称映射,避免逐行查询。
|
||||
func appContactOrgNames(ids []uint64) map[uint64]string {
|
||||
names := make(map[uint64]string)
|
||||
if len(ids) == 0 {
|
||||
return names
|
||||
}
|
||||
var orgs []models.BackendOrganization
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("id__in", ids).All(&orgs); err == nil {
|
||||
for _, o := range orgs {
|
||||
names[o.ID] = o.OrgName
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ContactList GET /app/oa/contact/list 通讯录分页列表。
|
||||
// 支持 keyword(姓名/手机号/邮箱/职位/部门)、org_id(按组织筛选)、contact_type(1 内部 2 外部)。
|
||||
func (c *AppOaController) ContactList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
orgID, _ := c.GetUint64("org_id")
|
||||
contactType, _ := c.GetInt("contact_type")
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("tid", claims.TenantId)
|
||||
if orgID > 0 {
|
||||
qs = qs.Filter("org_id", orgID)
|
||||
}
|
||||
if contactType > 0 {
|
||||
qs = qs.Filter("contact_type", contactType)
|
||||
}
|
||||
if keyword != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond = cond.Or("contact_name__contains", keyword)
|
||||
cond = cond.Or("phone__contains", keyword)
|
||||
cond = cond.Or("mobiles__contains", keyword)
|
||||
cond = cond.Or("email__contains", keyword)
|
||||
cond = cond.Or("position_title__contains", keyword)
|
||||
cond = cond.Or("dept_name__contains", keyword)
|
||||
// 用 GetCond().AndCond 追加,避免 SetCond 覆盖掉租户等前置过滤条件
|
||||
qs = qs.SetCond(qs.GetCond().AndCond(cond))
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var rows []models.BackendErpContact
|
||||
if _, err := qs.OrderBy("-is_starred", "contact_name", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询通讯录失败")
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]uint64, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.OrgID != nil && *r.OrgID > 0 {
|
||||
ids = append(ids, *r.OrgID)
|
||||
}
|
||||
}
|
||||
names := appContactOrgNames(ids)
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
orgName := ""
|
||||
if r.OrgID != nil {
|
||||
orgName = names[*r.OrgID]
|
||||
}
|
||||
list = append(list, appContactDTO(r, orgName))
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// ContactOrgTree GET /app/oa/contact/org-tree 组织架构树(含各部门内部联系人数量)。
|
||||
func (c *AppOaController) ContactOrgTree() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
var orgs []models.BackendOrganization
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("tid", claims.TenantId).
|
||||
OrderBy("sort", "id").All(&orgs); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询组织架构失败")
|
||||
return
|
||||
}
|
||||
|
||||
counts := make(map[uint64]int64)
|
||||
for _, org := range orgs {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("org_id", org.ID).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("contact_type", 1).Count()
|
||||
counts[org.ID] = cnt
|
||||
}
|
||||
|
||||
total, _ := models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).
|
||||
Filter("contact_type", 1).Count()
|
||||
|
||||
tree := (&BackendErpContactController{}).buildContactOrgTree(orgs, counts, uint64(total))
|
||||
c.oaOk(tree)
|
||||
}
|
||||
|
||||
// ContactDetail GET /app/oa/contact/:id 联系人详情。
|
||||
func (c *AppOaController) ContactDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的联系人ID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.BackendErpContact
|
||||
if models.Orm.QueryTable(new(models.BackendErpContact)).
|
||||
Filter("id", id).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).
|
||||
Exclude("status", 0).One(&row) != nil {
|
||||
c.oaErr(404, 404, "联系人不存在")
|
||||
return
|
||||
}
|
||||
|
||||
orgName := ""
|
||||
if row.OrgID != nil && *row.OrgID > 0 {
|
||||
var org models.BackendOrganization
|
||||
if models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("id", *row.OrgID).One(&org) == nil {
|
||||
orgName = org.OrgName
|
||||
}
|
||||
}
|
||||
|
||||
c.oaOk(appContactDTO(row, orgName))
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AppOaController 移动端 OA 协同办公接口(通知公告 / 文档库 / 薪酬工资单 / 员工档案)。
|
||||
//
|
||||
// 与 Web 管理端(/backend/oa/*)的区别:
|
||||
// - 鉴权放宽到 app 令牌(backend/app/platform),供 uniapp 调用;
|
||||
// - 只提供读能力(浏览 / 下载 / 收藏);写入操作仍留在 Web 端;
|
||||
// - 薪酬工资单与员工档案仅返回「本人」数据,通过登录账号匹配员工。
|
||||
//
|
||||
// 数据隔离:一律以 JWT 中的 TenantId 过滤,前端无法指定租户。
|
||||
type AppOaController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *AppOaController) oaOk(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// oaMyEmployee 解析当前登录用户对应的员工记录(tid + 账号匹配,口径与 backend_auth 一致)。
|
||||
func (c *AppOaController) oaMyEmployee(claims *jwtutil.Claims) (*models.BackendEmployee, bool) {
|
||||
uid := services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
|
||||
account := strings.TrimSpace(claims.Username)
|
||||
var tu models.SystemTenantUser
|
||||
if models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).One(&tu) == nil {
|
||||
if tu.Account != nil && strings.TrimSpace(*tu.Account) != "" {
|
||||
account = strings.TrimSpace(*tu.Account)
|
||||
}
|
||||
}
|
||||
if account == "" {
|
||||
return nil, false
|
||||
}
|
||||
var emp models.BackendEmployee
|
||||
if models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("tid", claims.TenantId).Filter("account", account).
|
||||
Filter("delete_time__isnull", true).One(&emp) != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &emp, true
|
||||
}
|
||||
|
||||
// ---------------- 通知公告 ----------------
|
||||
|
||||
// noticeItem 公告列表项(正文只回传摘要,避免列表过大)。
|
||||
func noticeItem(n models.OaNotice) map[string]interface{} {
|
||||
publishTime := ""
|
||||
if n.PublishTime != nil {
|
||||
publishTime = n.PublishTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"title": n.Title,
|
||||
"notice_type": n.NoticeType,
|
||||
"level": n.Level,
|
||||
"is_top": n.IsTop,
|
||||
"publisher_name": n.PublisherName,
|
||||
"publish_time": publishTime,
|
||||
"read_count": n.ReadCount,
|
||||
"summary": oaTextSummary(n.Content, 60),
|
||||
}
|
||||
}
|
||||
|
||||
// oaTextSummary 取正文纯文本摘要,按「字符数」截断(中文安全)。
|
||||
func oaTextSummary(content string, limit int) string {
|
||||
text := strings.Join(strings.Fields(content), " ")
|
||||
runes := []rune(text)
|
||||
if limit > 0 && len(runes) > limit {
|
||||
return string(runes[:limit]) + "…"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// NoticeList GET /app/oa/notice/list 仅返回已发布(status=1)的公告。
|
||||
func (c *AppOaController) NoticeList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("status", 1)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var rows []models.OaNotice
|
||||
_, err = qs.OrderBy("-is_top", "-publish_time", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, n := range rows {
|
||||
list = append(list, noticeItem(n))
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// NoticeDetail GET /app/oa/notice/:id 公告详情,同时累加阅读数。
|
||||
func (c *AppOaController) NoticeDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的公告ID")
|
||||
return
|
||||
}
|
||||
|
||||
var n models.OaNotice
|
||||
if err := models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("id", id).Filter("tid", claims.TenantId).
|
||||
Filter("is_deleted", 0).Filter("status", 1).One(&n); err != nil {
|
||||
c.oaErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = models.Orm.Raw(
|
||||
"UPDATE yz_backend_oa_notice SET read_count = read_count + 1 WHERE id = ?", id).Exec()
|
||||
n.ReadCount++
|
||||
|
||||
item := noticeItem(n)
|
||||
item["content"] = n.Content
|
||||
c.oaOk(item)
|
||||
}
|
||||
|
||||
// ---------------- 文档库 ----------------
|
||||
|
||||
// oaDocActor 由 JWT 构造文档访问者上下文(租户 + 用户可见性)。
|
||||
func (c *AppOaController) oaDocActor(claims *jwtutil.Claims) services.OaDocActor {
|
||||
return services.NewOaDocActor(claims.TenantId, uint64(claims.UserID))
|
||||
}
|
||||
|
||||
// docItem 文档列表/详情项(不回传体积较大的在线编辑源)。
|
||||
func docItem(d models.OaDoc) map[string]interface{} {
|
||||
updateTime := ""
|
||||
if d.UpdateTime != nil {
|
||||
updateTime = d.UpdateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": d.ID,
|
||||
"title": d.Title,
|
||||
"category_id": d.CategoryID,
|
||||
"file_url": d.FileURL,
|
||||
"file_name": d.FileName,
|
||||
"ext": d.Ext,
|
||||
"size": d.Size,
|
||||
"doc_type": d.DocType,
|
||||
"tags": d.Tags,
|
||||
"summary": d.Summary,
|
||||
"is_online": d.IsOnline,
|
||||
"status": d.Status,
|
||||
"version": d.Version,
|
||||
"is_star": d.IsStar,
|
||||
"owner_name": d.OwnerName,
|
||||
"creator_name": d.CreatorName,
|
||||
"view_count": d.ViewCount,
|
||||
"download_count": d.DownCount,
|
||||
"visibility": d.Visibility,
|
||||
"create_time": d.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": updateTime,
|
||||
}
|
||||
}
|
||||
|
||||
// DocCategories GET /app/oa/document/categories?scope=shared|personal|all 分类树(含数量)。
|
||||
func (c *AppOaController) DocCategories() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tree, err := services.OaDocCategoryTree(c.oaDocActor(claims), strings.TrimSpace(c.GetString("scope")))
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(tree)
|
||||
}
|
||||
|
||||
// DocList GET /app/oa/document/list 文档分页(按可见性 + 空间过滤)。
|
||||
func (c *AppOaController) DocList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
status, _ := c.GetInt("status", -1)
|
||||
docType, _ := c.GetInt("doc_type", -1)
|
||||
star, _ := c.GetInt("star", -1)
|
||||
categoryID, _ := c.GetUint64("category_id")
|
||||
|
||||
actor := c.oaDocActor(claims)
|
||||
list, total, err := services.OaDocList(actor, services.OaDocListParams{
|
||||
Actor: actor,
|
||||
Scope: strings.TrimSpace(c.GetString("scope")),
|
||||
Keyword: strings.TrimSpace(c.GetString("keyword")),
|
||||
CategoryID: categoryID,
|
||||
Uncategorized: strings.TrimSpace(c.GetString("uncategorized")) == "1",
|
||||
Visibility: -1,
|
||||
Mine: strings.TrimSpace(c.GetString("mine")) == "1",
|
||||
Status: status,
|
||||
DocType: docType,
|
||||
Star: star,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(list))
|
||||
for _, d := range list {
|
||||
items = append(items, docItem(d))
|
||||
}
|
||||
c.oaOk(map[string]interface{}{"list": items, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// DocDetail GET /app/oa/document/:id 文档详情(累加查看数)。
|
||||
func (c *AppOaController) DocDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
doc, err := services.OaDocView(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(404, 404, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(doc.Content) != "" {
|
||||
doc.IsOnline = 1
|
||||
}
|
||||
c.oaOk(docItem(*doc))
|
||||
}
|
||||
|
||||
// DocDownload GET /app/oa/document/download/:id 记录下载并返回下载地址。
|
||||
func (c *AppOaController) DocDownload() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
doc, err := services.OaDocDownload(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(404, 404, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(map[string]interface{}{
|
||||
"id": doc.ID,
|
||||
"title": doc.Title,
|
||||
"file_url": doc.FileURL,
|
||||
"file_name": doc.FileName,
|
||||
"download_count": doc.DownCount,
|
||||
})
|
||||
}
|
||||
|
||||
// DocStar POST /app/oa/document/:id/star 收藏 / 取消收藏。
|
||||
func (c *AppOaController) DocStar() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的文档ID")
|
||||
return
|
||||
}
|
||||
isStar, err := services.OaDocToggleStar(c.oaDocActor(claims), id)
|
||||
if err != nil {
|
||||
c.oaErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.oaOk(map[string]interface{}{"is_star": isStar})
|
||||
}
|
||||
|
||||
// ---------------- 薪酬工资单(仅本人) ----------------
|
||||
|
||||
// PayrollList GET /app/oa/compensation/payrolls 仅返回当前登录用户本人的薪资单。
|
||||
func (c *AppOaController) PayrollList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaOk(map[string]interface{}{"list": []map[string]interface{}{}, "total": 0})
|
||||
return
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.BackendOaPayroll)).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("employee_id", emp.ID).
|
||||
Filter("is_deleted", 0)
|
||||
if month := strings.TrimSpace(c.GetString("payroll_month")); compensationMonthPattern.MatchString(month) {
|
||||
qs = qs.Filter("payroll_month", month)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var rows []models.BackendOaPayroll
|
||||
if _, err := qs.OrderBy("-payroll_month", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, payrollMap(row))
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// PayrollDetail GET /app/oa/compensation/payrolls/:id 本人薪资单明细(含自定义增减项)。
|
||||
func (c *AppOaController) PayrollDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的薪资单ID")
|
||||
return
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaErr(404, 404, "未找到您的员工信息")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.BackendOaPayroll
|
||||
if models.Orm.QueryTable(new(models.BackendOaPayroll)).
|
||||
Filter("id", id).Filter("tid", claims.TenantId).
|
||||
Filter("employee_id", emp.ID).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.oaErr(404, 404, "薪资单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var items []models.BackendOaPayrollItem
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendOaPayrollItem)).
|
||||
Filter("tid", claims.TenantId).Filter("payroll_id", id).
|
||||
Filter("is_deleted", 0).OrderBy("sort_order", "id").All(&items)
|
||||
if items == nil {
|
||||
items = []models.BackendOaPayrollItem{}
|
||||
}
|
||||
|
||||
data := payrollMap(row)
|
||||
data["items"] = items
|
||||
c.oaOk(data)
|
||||
}
|
||||
|
||||
// ---------------- 员工档案(仅本人) ----------------
|
||||
|
||||
// MyEmployeeFile GET /app/oa/employee-file 返回当前登录用户本人的档案与子记录(只读)。
|
||||
func (c *AppOaController) MyEmployeeFile() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok {
|
||||
c.oaErr(404, 404, "未找到您的员工信息")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
var file models.BackendEmployeeFile
|
||||
if models.Orm.QueryTable(new(models.BackendEmployeeFile)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0).
|
||||
Filter("employee_id", emp.ID).One(&file) != nil {
|
||||
c.oaErr(404, 404, "您暂无员工档案")
|
||||
return
|
||||
}
|
||||
|
||||
var records []models.BackendEmployeeFileRecord
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0).
|
||||
Filter("employee_id", emp.ID).OrderBy("sort", "id").All(&records)
|
||||
if records == nil {
|
||||
records = []models.BackendEmployeeFileRecord{}
|
||||
}
|
||||
|
||||
dtos := (&BackendEmployeeFileController{}).efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
||||
|
||||
c.oaOk(map[string]interface{}{"file": dtos[0], "records": records})
|
||||
}
|
||||
|
||||
// ---------------- 工作台仪表盘(移动端首屏聚合) ----------------
|
||||
|
||||
// Dashboard GET /app/oa/dashboard
|
||||
//
|
||||
// 一次返回移动端 OA 首页顶部仪表盘所需的全部数据,口径对齐 Web 端
|
||||
// /backend/oa/schedule/stats 与 /backend/reimbursements/dashboard:
|
||||
//
|
||||
// {
|
||||
// schedule_stats:{today_total,today_done,week_total,week_done,month_total,month_done,overdue},
|
||||
// reimburse:{week,month,year,trend,status_dist,expense_types},
|
||||
// notices:[...], // 最近已发布公告(置顶优先,最多 5 条)
|
||||
// today_schedules:[...] // 今日日程(含已完成,灰显沉底)
|
||||
// }
|
||||
func (c *AppOaController) Dashboard() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
uid := uint64(claims.UserID)
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"schedule_stats": c.oaDashboardScheduleStats(claims.TenantId, uid),
|
||||
"reimburse": c.oaDashboardReimburse(uint64(claims.TenantId), uid),
|
||||
"notices": c.oaDashboardNotices(claims.TenantId),
|
||||
"today_schedules": c.oaDashboardTodaySchedules(claims.TenantId, uid),
|
||||
})
|
||||
}
|
||||
|
||||
// oaDashboardScheduleStats 今日/本周/本月日程统计与逾期数(与 Web 端 Stats 同口径)。
|
||||
func (c *AppOaController) oaDashboardScheduleStats(tid int, uid uint64) map[string]interface{} {
|
||||
now := time.Now()
|
||||
today := now.Format("2006-01-02")
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekStart := now.AddDate(0, 0, -(weekday - 1)).Format("2006-01-02")
|
||||
weekEnd := now.AddDate(0, 0, 7-weekday).Format("2006-01-02")
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
base := func() orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.OaSchedule)).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", tid).
|
||||
Filter("user_id", uid)
|
||||
}
|
||||
|
||||
todayTotal, _ := base().Filter("schedule_date", today).Count()
|
||||
todayDone, _ := base().Filter("schedule_date", today).Filter("status", 1).Count()
|
||||
weekTotal, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Count()
|
||||
weekDone, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Filter("status", 1).Count()
|
||||
monthTotal, _ := base().Filter("schedule_date__gte", monthStart).Count()
|
||||
monthDone, _ := base().Filter("schedule_date__gte", monthStart).Filter("status", 1).Count()
|
||||
overdue, _ := base().Filter("schedule_date__lt", today).Filter("status", 0).Count()
|
||||
|
||||
return map[string]interface{}{
|
||||
"today_total": todayTotal,
|
||||
"today_done": todayDone,
|
||||
"week_total": weekTotal,
|
||||
"week_done": weekDone,
|
||||
"month_total": monthTotal,
|
||||
"month_done": monthDone,
|
||||
"overdue": overdue,
|
||||
}
|
||||
}
|
||||
|
||||
// oaDashboardTodaySchedules 今日日程列表(未完成在前,已完成灰显沉底)。
|
||||
func (c *AppOaController) oaDashboardTodaySchedules(tid int, uid uint64) []map[string]interface{} {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var rows []models.OaSchedule
|
||||
_, _ = models.Orm.QueryTable(new(models.OaSchedule)).
|
||||
Filter("is_deleted", 0).Filter("tid", tid).Filter("user_id", uid).
|
||||
Filter("schedule_date", today).
|
||||
OrderBy("status", "start_time", "id").Limit(50).All(&rows)
|
||||
out := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, s := range rows {
|
||||
out = append(out, map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"title": s.Title,
|
||||
"schedule_date": s.ScheduleDate,
|
||||
"start_time": s.StartTime,
|
||||
"end_time": s.EndTime,
|
||||
"all_day": s.AllDay,
|
||||
"color": s.Color,
|
||||
"priority": s.Priority,
|
||||
"status": s.Status,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// oaDashboardNotices 最近已发布公告(置顶优先,最多 5 条)。
|
||||
func (c *AppOaController) oaDashboardNotices(tid int) []map[string]interface{} {
|
||||
var rows []models.OaNotice
|
||||
_, _ = models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("is_deleted", 0).Filter("tid", tid).Filter("status", 1).
|
||||
OrderBy("-is_top", "-publish_time", "-id").Limit(5).All(&rows)
|
||||
out := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, n := range rows {
|
||||
out = append(out, noticeItem(n))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// oaDashboardReimburse 报销统计(周/月/年 + 近6月趋势 + 状态分布 + 类型分析),
|
||||
// 个人报销单量小,一次全量查询内存聚合(与 Web 端 Dashboard 同口径)。
|
||||
func (c *AppOaController) oaDashboardReimburse(tid, uid uint64) map[string]interface{} {
|
||||
now := time.Now()
|
||||
y, m, _ := now.Date()
|
||||
today := time.Date(y, m, now.Day(), 0, 0, 0, 0, now.Location())
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
weekday := int(today.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
weekStart := today.AddDate(0, 0, -(weekday - 1))
|
||||
monthStart := time.Date(y, m, 1, 0, 0, 0, 0, now.Location())
|
||||
yearStart := time.Date(y, 1, 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
newStat := func() map[string]interface{} {
|
||||
return map[string]interface{}{"total": 0.0, "paid": 0.0, "unpaid": 0.0, "count": 0}
|
||||
}
|
||||
addTo := func(stat map[string]interface{}, row *models.BackendReimbursement) {
|
||||
stat["total"] = stat["total"].(float64) + row.TotalAmount
|
||||
stat["count"] = stat["count"].(int) + 1
|
||||
if row.Status == 5 {
|
||||
stat["paid"] = stat["paid"].(float64) + row.TotalAmount
|
||||
} else {
|
||||
stat["unpaid"] = stat["unpaid"].(float64) + row.TotalAmount
|
||||
}
|
||||
}
|
||||
inRange := func(t, start, end time.Time) bool {
|
||||
return !t.Before(start) && t.Before(end)
|
||||
}
|
||||
|
||||
weekStat, monthStat, yearStat := newStat(), newStat(), newStat()
|
||||
monthKeys := make([]string, 0, 6)
|
||||
monthStats := map[string]map[string]interface{}{}
|
||||
for i := 5; i >= 0; i-- {
|
||||
key := time.Date(y, m-time.Month(i), 1, 0, 0, 0, 0, now.Location()).Format("2006-01")
|
||||
monthKeys = append(monthKeys, key)
|
||||
monthStats[key] = newStat()
|
||||
}
|
||||
statusNames := map[int8]string{0: "草稿", 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回", 5: "已打款"}
|
||||
statusDist := map[string]interface{}{}
|
||||
|
||||
var rows []models.BackendReimbursement
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursement)).
|
||||
Filter("tid", tid).Filter("user_id", uid).Filter("is_deleted", 0).
|
||||
All(&rows); err != nil {
|
||||
return map[string]interface{}{
|
||||
"week": weekStat, "month": monthStat, "year": yearStat,
|
||||
"trend": []map[string]interface{}{}, "status_dist": statusDist, "expense_types": []map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
row := &rows[i]
|
||||
created := row.CreateTime
|
||||
if inRange(created, weekStart, tomorrow) {
|
||||
addTo(weekStat, row)
|
||||
}
|
||||
if inRange(created, monthStart, tomorrow) {
|
||||
addTo(monthStat, row)
|
||||
}
|
||||
if inRange(created, yearStart, tomorrow) {
|
||||
addTo(yearStat, row)
|
||||
}
|
||||
if st, ok := monthStats[created.Format("2006-01")]; ok {
|
||||
addTo(st, row)
|
||||
}
|
||||
sk := strconv.Itoa(int(row.Status))
|
||||
dist, ok := statusDist[sk].(map[string]interface{})
|
||||
if !ok {
|
||||
dist = map[string]interface{}{"name": statusNames[row.Status], "count": 0, "amount": 0.0}
|
||||
statusDist[sk] = dist
|
||||
}
|
||||
dist["count"] = dist["count"].(int) + 1
|
||||
dist["amount"] = dist["amount"].(float64) + row.TotalAmount
|
||||
}
|
||||
|
||||
trend := make([]map[string]interface{}, 0, len(monthKeys))
|
||||
for _, key := range monthKeys {
|
||||
st := monthStats[key]
|
||||
trend = append(trend, map[string]interface{}{
|
||||
"month": key, "total": st["total"], "paid": st["paid"],
|
||||
"unpaid": st["unpaid"], "count": st["count"],
|
||||
})
|
||||
}
|
||||
|
||||
// 报销类型分析:仅统计已打款(status=5)的报销单费用明细
|
||||
paidIDs := map[uint64]bool{}
|
||||
for i := range rows {
|
||||
if rows[i].Status == 5 {
|
||||
paidIDs[rows[i].ID] = true
|
||||
}
|
||||
}
|
||||
expenseTypes := []map[string]interface{}{}
|
||||
var items []models.BackendReimbursementItem
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("tid", tid).Filter("uid", uid).Filter("is_deleted", 0).
|
||||
All(&items); err == nil {
|
||||
typeAgg := map[string]*struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
for name, agg := range typeAgg {
|
||||
expenseTypes = append(expenseTypes, map[string]interface{}{
|
||||
"name": name, "count": agg.count, "amount": agg.amount,
|
||||
})
|
||||
}
|
||||
sort.Slice(expenseTypes, func(i, j int) bool {
|
||||
return expenseTypes[i]["amount"].(float64) > expenseTypes[j]["amount"].(float64)
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"week": weekStat, "month": monthStat, "year": yearStat,
|
||||
"trend": trend, "status_dist": statusDist, "expense_types": expenseTypes,
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,8 @@ func (c *AppReminderController) GetList() {
|
||||
if len(reminders) > 0 {
|
||||
first := reminders[0]
|
||||
item["advance_minutes"] = first.AdvanceMinutes
|
||||
item["repeat_interval_minutes"] = first.RepeatIntervalMinutes
|
||||
item["max_send_count"] = first.MaxSendCount
|
||||
if !first.CreateTime.IsZero() {
|
||||
item["created_at"] = first.CreateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -227,6 +229,8 @@ func (c *AppReminderController) GetDetail() {
|
||||
}
|
||||
if first.ID > 0 {
|
||||
data["advance_minutes"] = first.AdvanceMinutes
|
||||
data["repeat_interval_minutes"] = first.RepeatIntervalMinutes
|
||||
data["max_send_count"] = first.MaxSendCount
|
||||
if !first.CreateTime.IsZero() {
|
||||
data["created_at"] = first.CreateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
@@ -108,15 +108,18 @@ func (c *AuthLoginController) LoginSubmit() {
|
||||
}
|
||||
|
||||
// 开启人机验证时:先极验通过,再走账号密码校验
|
||||
if err := authsvc.VerifyLoginCaptcha(0, authsvc.CaptchaResult{
|
||||
CaptchaID: req.CaptchaID,
|
||||
LotNumber: req.LotNumber,
|
||||
PassToken: req.PassToken,
|
||||
GenTime: req.GenTime,
|
||||
CaptchaOutput: req.CaptchaOutput,
|
||||
}); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
// 小程序端(yz-miniapp)无法集成极验 SDK 且运行于微信沙箱内,跳过人机验证
|
||||
if strings.TrimSpace(req.ClientID) != "yz-miniapp" {
|
||||
if err := authsvc.VerifyLoginCaptcha(0, authsvc.CaptchaResult{
|
||||
CaptchaID: req.CaptchaID,
|
||||
LotNumber: req.LotNumber,
|
||||
PassToken: req.PassToken,
|
||||
GenTime: req.GenTime,
|
||||
CaptchaOutput: req.CaptchaOutput,
|
||||
}); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clientIP := c.Ctx.Input.IP()
|
||||
|
||||
@@ -177,6 +177,59 @@ func (c *AuthWechatMiniController) Login() {
|
||||
c.serveJSON(resp)
|
||||
}
|
||||
|
||||
// Bind 把当前小程序 openid 绑定到已登录身份
|
||||
// POST /auth/wechat/miniapp/bind { "code": "wx.login的code" } Header: Bearer 令牌
|
||||
//
|
||||
// 用途:小程序认证中 / 未开通「手机号快速验证」时,getPhoneNumber 拿不到手机号,
|
||||
// 一键登录退化为「按 openid 匹配已绑定身份」。用户先用账号密码登录一次,
|
||||
// 前端调用本接口静默绑定 openid,之后即可无手机号完成微信一键登录。
|
||||
// 显式绑定视为本人操作(同时持有微信与该账号登录态),允许换绑覆盖旧绑定。
|
||||
func (c *AuthWechatMiniController) Bind() {
|
||||
claims := claimsFromCtxThird(c.Ctx)
|
||||
if claims == nil || claims.UserID <= 0 {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.fail("", 400, "参数错误")
|
||||
return
|
||||
}
|
||||
req.Code = strings.TrimSpace(req.Code)
|
||||
if req.Code == "" {
|
||||
c.fail("", 400, "缺少登录凭证,请重新进入小程序")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := wechatmini.LoadEnabledConfig()
|
||||
if err != nil {
|
||||
c.fail(MiniReasonNotConfigured, 400, "小程序登录未开通")
|
||||
return
|
||||
}
|
||||
|
||||
session, err := wechatmini.Code2Session(cfg, req.Code)
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
if hint, ok := wechatmini.ExplainError(err); ok {
|
||||
msg = hint
|
||||
}
|
||||
logs.Warn("[wechat-mini] 绑定 code 换取 openid 失败: %v", err)
|
||||
c.fail(MiniReasonWechatAPI, 400, msg)
|
||||
return
|
||||
}
|
||||
|
||||
c.syncBinding(cfg, uint64(claims.UserID), session, true)
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "绑定成功"})
|
||||
}
|
||||
|
||||
// resolveIdentity 定位身份并维护小程序绑定关系。
|
||||
// mobile 非空表示本次已通过微信手机号验证,是可信的强证据,优先采用。
|
||||
func (c *AuthWechatMiniController) resolveIdentity(cfg *wechatmini.Config, mobile string, session *wechatmini.Session) (*models.AuthIdentity, error) {
|
||||
|
||||
Reference in new issue
Block a user